From 6ae711071dd1cd36540962cbb3eb87a063a494c7 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sat, 29 Aug 2026 11:26:03 +0100 Subject: [PATCH 01/16] Move out isAttacking squash --- .../AttackTimerMetronomePlugin.java | 58 +-------- .../com/attacktimer/Attacking/Attacking.java | 114 ++++++++++++++++++ 2 files changed, 117 insertions(+), 55 deletions(-) create mode 100644 src/main/java/com/attacktimer/Attacking/Attacking.java diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java index 5e385c6..f7afd3a 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java @@ -27,6 +27,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import com.attacktimer.Attacking.Attacking; import com.attacktimer.ClientUtils.Utils; import com.attacktimer.VariableSpeed.State.TickCount; import com.attacktimer.VariableSpeed.VariableSpeed; @@ -39,8 +40,6 @@ import java.awt.Dimension; import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; @@ -385,57 +384,6 @@ private int getWeaponSpeed(int weaponId, PoweredStaves stave, AnimationData curA return VariableSpeed.compute(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, spellbook, dmgDealt, specDelta, aspeed); } - // Combat Dummy + Nightmare Pillars - private static final List SPECIAL_NPCS = Arrays.asList(10507, 9435, 9438, 9441, 9444); - - private boolean isPlayerAttacking() - { - final Player localPlayer = client.getLocalPlayer(); - final int animationId = localPlayer.getAnimation(); - if (AnimationData.isBlockListAnimation(animationId)) - { - return false; - } - - // Not walking is either ANY player animation or the edge cases which don't trigger an animation, e.g Salamander. - final boolean notWalking = animationId != -1 || getSalamanderAttack(); - - // Testing if we are attacking by checking the target is more future proof to new weapons which don't - // need custom code and the weapon stats are enough. - final Actor target = localPlayer.getInteracting(); - if (target != null && (target instanceof NPC)) - { - final NPC npc = (NPC) target; - final boolean containsAttackOption = Arrays.stream(npc.getComposition().getActions()) - .anyMatch("Attack"::equals); - final Integer health = npcManager.getHealth(npc.getId()); - final boolean hasHealthAndLevel = health != null && health > 0 && target.getCombatLevel() > 0; - final boolean attackingNPC = hasHealthAndLevel || SPECIAL_NPCS.contains(npc.getId()) - || containsAttackOption; - // just having a target is not enough the player may be out of range, we must wait for any - // animation which isn't running/walking/etc - return attackingNPC && notWalking; - } - if (target != null && (target instanceof Player)) - { - return notWalking; - } - if (target == null) - { - // Not attacking anything - return false; - } - - // Do not use any animations from this set - final AnimationData fromId = AnimationData.fromId(animationId); - if (UNRELIABLE_ANIMATIONS.contains(fromId)) - { - return false; - } - // fall back to animations. - return fromId != null; - } - private boolean isManualCasting(AnimationData curId) { // If you use a weapon like a blow pipe which has an animation longer than it's cool down then cast an @@ -525,7 +473,7 @@ public void onInteractingChanged(InteractingChanged interactingChanged) isUsingMagic = false; // If not previously attacking, this action can result in a queued attack or // an instant attack. If its queued, don't trigger the cooldown yet. - if (isPlayerAttacking()) + if (Attacking.isPlayerAttacking(client, npcManager)) { logStateTrace("onInteractingChanged"); performAttack(); @@ -555,7 +503,7 @@ public void onGameTick(GameTick tick) if (!config.enableMetronome()) return; VariableSpeed.onGameTick(client, tick); - boolean isAttacking = isPlayerAttacking(); + final boolean isAttacking = Attacking.isPlayerAttacking(client, npcManager); switch (attackState) { case NOT_ATTACKING: diff --git a/src/main/java/com/attacktimer/Attacking/Attacking.java b/src/main/java/com/attacktimer/Attacking/Attacking.java new file mode 100644 index 0000000..355b25b --- /dev/null +++ b/src/main/java/com/attacktimer/Attacking/Attacking.java @@ -0,0 +1,114 @@ +package com.attacktimer.Attacking; + +/* + * Copyright (c) 2022, Nick Graves + * Copyright (c) 2024-2026, Lexer747 + * Copyright (c) 2024-2026, Richardant + * 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 com.attacktimer.AnimationData; +import com.google.common.collect.ImmutableSet; +import java.util.Arrays; +import java.util.Set; +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.gameval.NpcID; +import net.runelite.api.gameval.SpotanimID; +import net.runelite.client.game.NPCManager; + +public class Attacking +{ + // These animations are the ones which exceed the duration of their attack cooldown + // so in this case DO NOT fall back the animation as it is un-reliable. + private static final Set UNRELIABLE_ANIMATIONS = new ImmutableSet.Builder() + .add(AnimationData.RANGED_BLOWPIPE).add(AnimationData.RANGED_BLAZING_BLOWPIPE) + .add(AnimationData.MAGIC_EYE_OF_AYAK).add(AnimationData.MAGIC_EYE_OF_AYAK_SPEC).build(); + + // Combat Dummy + Nightmare Pillars + private static final Set SPECIAL_NPCS = new ImmutableSet.Builder() + .add(NpcID.POH_COMBAT_DUMMY_UPGRADED_UNDEADSLAYER_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_ETHER_NPC, + NpcID.POH_COMBAT_DUMMY_UPGRADED_KQ_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_KURASK_NPC, + NpcID.POH_COMBAT_DUMMY_UPGRADED_VAMPIRE_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_VORKATH_NPC, + NpcID.POH_COMBAT_DUMMY_NPC, NpcID.POH_COMBAT_DUMMY_UNDEADSLAYER_NPC, + NpcID.POH_COMBAT_DUMMY_UPGRADED_NPC, NpcID.NIGHTMARE_TOTEM_1_READY, NpcID.NIGHTMARE_TOTEM_2_READY, + NpcID.NIGHTMARE_TOTEM_3_READY, NpcID.NIGHTMARE_TOTEM_4_READY) + .build(); + + public static boolean isPlayerAttacking(final Client client, final NPCManager npcManager) + { + final Player localPlayer = client.getLocalPlayer(); + final int animationId = localPlayer.getAnimation(); + if (AnimationData.isBlockListAnimation(animationId)) + { + return false; + } + + // Not walking is either ANY player animation or the edge cases which don't trigger an animation, + // e.g Salamander. + final boolean notWalking = animationId != -1 || getSalamanderAttack(client); + + // Testing if we are attacking by checking the target is more future proof to new weapons which + // don't + // need custom code and the weapon stats are enough. + final Actor target = localPlayer.getInteracting(); + if (target != null && (target instanceof NPC)) + { + final NPC npc = (NPC) target; + final boolean containsAttackOption = Arrays.stream(npc.getComposition().getActions()) + .anyMatch("Attack"::equals); + final Integer health = npcManager.getHealth(npc.getId()); + final boolean hasHealthAndLevel = health != null && health > 0 && target.getCombatLevel() > 0; + final boolean attackingNPC = hasHealthAndLevel || SPECIAL_NPCS.contains(npc.getId()) + || containsAttackOption; + // just having a target is not enough the player may be out of range, we must wait for any + // animation which isn't running/walking/etc + return attackingNPC && notWalking; + } + if (target != null && (target instanceof Player)) + { + return notWalking; + } + if (target == null) + { + // Not attacking anything + return false; + } + + // Do not use any animations from this set + final AnimationData fromId = AnimationData.fromId(animationId); + if (UNRELIABLE_ANIMATIONS.contains(fromId)) + { + return false; + } + // fall back to animations. + return fromId != null; + } + + private static boolean getSalamanderAttack(final Client client) + { + return client.getLocalPlayer().hasSpotAnim(SpotanimID.FIREBREATH); + } +} From 607fef60e531988583a642f41aa6cbd30f06137a Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sat, 29 Aug 2026 11:26:15 +0100 Subject: [PATCH 02/16] stub maggot king --- .../attacktimer/VariableSpeed/MaggotKing.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java diff --git a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java new file mode 100644 index 0000000..d9ec81d --- /dev/null +++ b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java @@ -0,0 +1,44 @@ +package com.attacktimer.VariableSpeed; + +/* + * 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 net.runelite.api.Client; +import com.attacktimer.ClientUtils.Utils; + +public class MaggotKing +{ + private static final int MAGGOT_KING_REGION_ID = -1; + + MaggotKing() {} + + public int onRender(final Client client, final int attackDelayHoldoffTicks, final boolean isUsingMagic, final boolean debugLogs) + { + if (!Utils.isInRegionId(client, MAGGOT_KING_REGION_ID)) + { + return 0; + } + } +} From ac97f7bb20d391ef2aab3d2ea838e8ad9f908888 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sat, 29 Aug 2026 13:31:27 +0100 Subject: [PATCH 03/16] implement maggot king, untested --- .../java/com/attacktimer/AnimationData.java | 6 ++ .../AttackTimerMetronomePlugin.java | 50 +--------------- .../com/attacktimer/Attacking/Attack.java | 46 +++++++++++++++ .../com/attacktimer/Attacking/Attacking.java | 8 +++ .../com/attacktimer/ClientUtils/Utils.java | 57 +++++++++++++++---- .../attacktimer/VariableSpeed/MaggotKing.java | 56 ++++++++++++++++-- .../VariableSpeed/VariableSpeed.java | 4 +- 7 files changed, 164 insertions(+), 63 deletions(-) create mode 100644 src/main/java/com/attacktimer/Attacking/Attack.java diff --git a/src/main/java/com/attacktimer/AnimationData.java b/src/main/java/com/attacktimer/AnimationData.java index e905d5f..a8b0a9c 100644 --- a/src/main/java/com/attacktimer/AnimationData.java +++ b/src/main/java/com/attacktimer/AnimationData.java @@ -418,4 +418,10 @@ public String toString() return StringUtils.capitalize(super.toString().toLowerCase()); } } + + + public boolean isStandardBowAttack() + { + throw new UnsupportedOperationException("Unimplemented method 'isStandardBowAttack'"); + } } diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java index f7afd3a..a0bfaa8 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java @@ -144,33 +144,10 @@ public enum AttackState private static final int ATTACK_DELAY_NONE = 0; public static final int DEFAULT_SIZE_UNIT_PX = 25; - // Add other weapons here if in the Runelite dev shell this prints a different value to it's actual speed: - // - // var itemManager = inject(ItemManager.class); - // log.info("Speed {}", itemManager.getItemStats().getEquipment().getAspeed()); - private static final Map NON_STANDARD_ATTACK_SPEEDS = new ImmutableMap.Builder() - .put(ItemID.HALLOWFELL, 6) - .build(); - - // These animations are the ones which exceed the duration of their attack cooldown - // so in this case DO NOT fall back the animation as it is un-reliable. - private static final Set UNRELIABLE_ANIMATIONS = new ImmutableSet.Builder() - .add(AnimationData.RANGED_BLOWPIPE) - .add(AnimationData.RANGED_BLAZING_BLOWPIPE) - .add(AnimationData.MAGIC_EYE_OF_AYAK) - .add(AnimationData.MAGIC_EYE_OF_AYAK_SPEC) - .build(); - private static final Map NON_STANDARD_MAGIC_WEAPON_SPEEDS = new ImmutableMap.Builder() .put(ItemID.TWINFLAME_STAFF, 6) .build(); - // Map of problematic itemIds to equivalent working ones. - // The Echo Venator Bow's ItemStats are returning null, so use the regular bow instead. - private static final Map WEAPON_ID_MAPPING_WORKAROUNDS = new ImmutableMap.Builder() - .put(ItemID.VENATOR_BOW_ORNAMENT, ItemID.VENATOR_BOW) - .build(); - // https://oldschool.runescape.wiki/w/Food/Fast_foods#Food_Delays // These constants are not to be confused with eat delay. private final int SLOW_FOOD_ATTACK_DELAY_TICKS = 4; @@ -300,30 +277,9 @@ AttackTimerMetronomeConfig provideConfig(ConfigManager configManager) return configManager.getConfig(AttackTimerMetronomeConfig.class); } - private int getWeaponId() - { - final int weaponId = Utils.getWeaponId(client); - return WEAPON_ID_MAPPING_WORKAROUNDS.getOrDefault(weaponId, weaponId); - } - - private ItemStats getWeaponStats(int weaponId) - { - if (NON_STANDARD_ATTACK_SPEEDS.containsKey(weaponId)) - { - return new ItemStats(true, -1, -1, - ItemEquipmentStats.builder().aspeed(NON_STANDARD_ATTACK_SPEEDS.get(weaponId)).build()); - } - return itemManager.getItemStats(weaponId); - } - - private boolean getSalamanderAttack() - { - return client.getLocalPlayer().hasSpotAnim(SpotanimID.FIREBREATH); - } - private void setAttackDelay() { - int weaponId = getWeaponId(); + int weaponId = Utils.getWeaponId(client); AnimationData curAnimation = AnimationData.fromId(client.getLocalPlayer().getAnimation()); PoweredStaves stave = PoweredStaves.getPoweredStaves(weaponId, curAnimation); boolean matchesSpellbook = matchesSpellbook(curAnimation); @@ -373,7 +329,7 @@ private int getWeaponSpeed(int weaponId, PoweredStaves stave, AnimationData curA } isUsingMagic = false; - final ItemStats weaponStats = getWeaponStats(weaponId); + final ItemStats weaponStats = Utils.getWeaponStats(client, itemManager, weaponId); if (weaponStats == null) { // Assume barehanded == 4t @@ -625,7 +581,7 @@ public void onRender() public void checkForLateWeaponSwaps() { - final boolean weaponMisMatch = getWeaponId() != lastUsedWeaponId; + final boolean weaponMisMatch = Utils.getWeaponId(client) != lastUsedWeaponId; // This windowing safe guards of from late swaps inside a tick, if we have already rendered the tick // then we shouldn't perform another attack. We don't need to check for a valid target diff --git a/src/main/java/com/attacktimer/Attacking/Attack.java b/src/main/java/com/attacktimer/Attacking/Attack.java new file mode 100644 index 0000000..03bd7d7 --- /dev/null +++ b/src/main/java/com/attacktimer/Attacking/Attack.java @@ -0,0 +1,46 @@ +package com.attacktimer.Attacking; + +/* + * 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 lombok.Getter; +import net.runelite.api.Actor; + +public final class Attack +{ + @Getter + private final int animationId; + @Getter + private final boolean salamander; + @Getter + private final Actor target; + + public Attack(final int animationId, final boolean salamander, final Actor target) + { + this.animationId = animationId; + this.salamander = salamander; + this.target = target; + } +} diff --git a/src/main/java/com/attacktimer/Attacking/Attacking.java b/src/main/java/com/attacktimer/Attacking/Attacking.java index 355b25b..c06d9d5 100644 --- a/src/main/java/com/attacktimer/Attacking/Attacking.java +++ b/src/main/java/com/attacktimer/Attacking/Attacking.java @@ -107,6 +107,14 @@ public static boolean isPlayerAttacking(final Client client, final NPCManager np return fromId != null; } + public static Attack PlayerAttack(final Client client) + { + final Player localPlayer = client.getLocalPlayer(); + final int animationId = localPlayer.getAnimation(); + final Actor target = localPlayer.getInteracting(); + return new Attack(animationId, getSalamanderAttack(client), target); + } + private static boolean getSalamanderAttack(final Client client) { return client.getLocalPlayer().hasSpotAnim(SpotanimID.FIREBREATH); diff --git a/src/main/java/com/attacktimer/ClientUtils/Utils.java b/src/main/java/com/attacktimer/ClientUtils/Utils.java index 15114a3..145a49e 100644 --- a/src/main/java/com/attacktimer/ClientUtils/Utils.java +++ b/src/main/java/com/attacktimer/ClientUtils/Utils.java @@ -29,7 +29,9 @@ import com.attacktimer.AttackStyle; import com.attacktimer.AttackType; import com.attacktimer.WeaponType; +import com.google.common.collect.ImmutableMap; import java.util.ArrayDeque; +import java.util.Map; import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.EquipmentInventorySlot; @@ -40,13 +42,17 @@ import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; import net.runelite.api.gameval.InventoryID; +import net.runelite.api.gameval.ItemID; import net.runelite.api.gameval.VarPlayerID; import net.runelite.api.gameval.VarbitID; +import net.runelite.client.game.ItemEquipmentStats; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.ItemStats; import org.apache.commons.lang3.ArrayUtils; public class Utils { - public static int getItemIdFromContainer(ItemContainer container, int slotID) + public static int getItemIdFromContainer(final ItemContainer container, final int slotID) { if (container == null) { @@ -56,7 +62,7 @@ public static int getItemIdFromContainer(ItemContainer container, int slotID) return (item != null) ? item.getId() : -1; } - public static int getWeaponId(Client client) + public static int getWeaponIdRaw(final Client client) { return getItemIdFromContainer(client.getItemContainer(InventoryID.WORN), EquipmentInventorySlot.WEAPON.getSlotIdx()); @@ -66,14 +72,14 @@ public static int getWeaponId(Client client) // // For computing tile based distances you probably don't want this and instead should use // client.getLocalPlayer().getWorldLocation(). - public static WorldPoint getLocalLocation(Client client) + public static WorldPoint getLocalLocation(final Client client) { final LocalPoint localPoint = client.getLocalPlayer().getLocalLocation(); return WorldPoint.fromLocalInstance(client, localPoint); } // returns ACCURATE for unknown weapons/styles - public static AttackStyle getAttackStyle(Client client) + public static AttackStyle getAttackStyle(final Client client) { final AttackStyle[] attackStyles = getWeaponType(client).getAttackStyles(client); int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE); @@ -94,14 +100,14 @@ public static AttackStyle getAttackStyle(Client client) } // returns null for unknown weapons - public static WeaponType getWeaponType(Client client) + public static WeaponType getWeaponType(final Client client) { final int currentEquippedWeaponTypeVarbit = client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY); return WeaponType.getWeaponType(currentEquippedWeaponTypeVarbit); } // returns null for unknown weapons - public static AttackType getAttackType(Client client) + public static AttackType getAttackType(final Client client) { final WeaponType weaponType = getWeaponType(client); final int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE); @@ -113,7 +119,7 @@ public static AttackType getAttackType(Client client) } // returns zero for no target - public static int getTargetId(Client client) + public static int getTargetId(final Client client) { final NPC target = getTargetNPC(client); int targetId = 0; @@ -125,7 +131,7 @@ public static int getTargetId(Client client) } // returns null for no target - public static NPC getTargetNPC(Client client) + public static NPC getTargetNPC(final Client client) { final Actor target = client.getLocalPlayer().getInteracting(); if (target != null && (target instanceof NPC)) @@ -137,7 +143,7 @@ public static NPC getTargetNPC(Client client) } // returns true if the client is in the region specified by the id - public static boolean isInRegionId(Client client, int id) + public static boolean isInRegionId(final Client client, final int id) { final WorldView wv = client.getTopLevelWorldView(); if (wv == null) @@ -157,7 +163,7 @@ public static boolean isInRegionId(Client client, int id) // getLastDelta gets the last two elements and returns the delta between the two items. It does not modify // the queue. Returns 0 if theres no items in the queue, returns + 1 if there's only 1 item in // the queue. - public static int getLastDelta(ArrayDeque events) + public static int getLastDelta(final ArrayDeque events) { int i = 0, last = -1, secondLast = -1; final var it = events.descendingIterator(); @@ -174,4 +180,35 @@ else if (i == 1) var delta = last - secondLast; return delta; } + + // Map of problematic itemIds to equivalent working ones. + // The Echo Venator Bow's ItemStats are returning null, so use the regular bow instead. + private static final Map WEAPON_ID_MAPPING_WORKAROUNDS = new ImmutableMap.Builder() + .put(ItemID.VENATOR_BOW_ORNAMENT, ItemID.VENATOR_BOW) + .build(); + + // Add other weapons here if in the Runelite dev shell this prints a different value to it's actual + // speed: + // + // var itemManager = inject(ItemManager.class); + // log.info("Speed {}", itemManager.getItemStats().getEquipment().getAspeed()); + private static final Map NON_STANDARD_ATTACK_SPEEDS = new ImmutableMap.Builder() + .put(ItemID.HALLOWFELL, 6) + .build(); + + public static int getWeaponId(final Client client) + { + final int weaponId = Utils.getWeaponIdRaw(client); + return WEAPON_ID_MAPPING_WORKAROUNDS.getOrDefault(weaponId, weaponId); + } + + public static ItemStats getWeaponStats(final Client client, final ItemManager itemManager, final int weaponId) + { + if (NON_STANDARD_ATTACK_SPEEDS.containsKey(weaponId)) + { + return new ItemStats(true, -1, -1, + ItemEquipmentStats.builder().aspeed(NON_STANDARD_ATTACK_SPEEDS.get(weaponId)).build()); + } + return itemManager.getItemStats(weaponId); + } } diff --git a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java index d9ec81d..0b12b47 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java +++ b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java @@ -25,20 +25,68 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import net.runelite.api.Client; +import com.attacktimer.AnimationData; +import com.attacktimer.AttackProcedure; +import com.attacktimer.Attacking.Attacking; import com.attacktimer.ClientUtils.Utils; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.gameval.NpcID; +import net.runelite.client.game.ItemManager; +import net.runelite.client.game.ItemStats; +@Slf4j public class MaggotKing { private static final int MAGGOT_KING_REGION_ID = -1; - MaggotKing() {} + MaggotKing() + {} - public int onRender(final Client client, final int attackDelayHoldoffTicks, final boolean isUsingMagic, final boolean debugLogs) + // https://oldschool.runescape.wiki/w/Maggot_King/Strategies#Ur-maggot_larvae + // + // If the player attacks a maggot with a "standard bow" it doesn't matter what current cooldown is the + // player is immediately set to the cooldown of the bow they used. + // + // Therefore this method returns `attackDelayHoldoffTicks` in all cases where this condition isn't met. + // But if the condition is met this method returns a brand new number which is the attack speed of the bow + // used. This number can be the same as the current delay and that's ok. + public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, + final boolean debugLogs) { if (!Utils.isInRegionId(client, MAGGOT_KING_REGION_ID)) { - return 0; + return attackDelayHoldoffTicks; + } + + final var atk = Attacking.PlayerAttack(client); + final AnimationData anim = AnimationData.fromId(atk.getAnimationId()); + if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC)) + { + return attackDelayHoldoffTicks; + } + + final NPC npc = (NPC) atk.getTarget(); + if (npc.getId() != NpcID.UR_MAGGOT_LARVAE) + { + return attackDelayHoldoffTicks; + } + + if (!anim.isStandardBowAttack()) + { + return attackDelayHoldoffTicks; } + if (debugLogs) + { + log.debug("MaggotKing success, attacking maggot with bow"); + } + + final ItemStats weaponStats = Utils.getWeaponStats(client, itemManager, Utils.getWeaponId(client)); + final int aspeed = weaponStats.getEquipment().getAspeed(); + // We don't want a full variable speed here, we know apriori that none of them will apply (leagues + // will but that's hard to test and changes every time it comes around) + return VariableSpeed.RAPID_ATTACK_STYLE.apply(client, anim, AttackProcedure.MELEE_OR_RANGE, null, -1, -1, + aspeed, aspeed); } } diff --git a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java index eeba750..079f1ed 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java +++ b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java @@ -105,6 +105,7 @@ public static void onNpcDespawned(final Client client, final NpcDespawned npcDes private static final Yama YAMA = new Yama(); private static final MarkOfDarkness MARK_OF_DARKNESS = new MarkOfDarkness(); + public static final RapidAttackStyle RAPID_ATTACK_STYLE = new RapidAttackStyle(); private static final IStateTracker[] TO_TRACK = { // State tracking, these do not contribute themselves to any variable speed weapon/mechanic but @@ -120,7 +121,7 @@ public static void onNpcDespawned(final Client client, final NpcDespawned npcDes // Incremental: new BloodMoonSet(), - new RapidAttackStyle(), + RAPID_ATTACK_STYLE, new RedKerisSpec(), new PurgingStaffSpec(YAMA), new EyeOfAyak(), @@ -136,5 +137,4 @@ public static void onNpcDespawned(final Client client, final NpcDespawned npcDes // Variable speed that doesn't neatly fit in to the IVariable speed pattern (it's not weapon related // but boss related). public static final ShadowCrash SHADOW_CRASH = new ShadowCrash(YAMA, MARK_OF_DARKNESS, AttackTimerMetronomePlugin.TC); - } From 76f416356f39c1e5492497e780090d302a00f417 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sat, 29 Aug 2026 14:25:44 +0100 Subject: [PATCH 04/16] populate with test data --- .../java/com/attacktimer/AnimationData.java | 215 ++++++++++-------- .../AttackTimerMetronomePlugin.java | 4 - .../attacktimer/VariableSpeed/MaggotKing.java | 2 +- 3 files changed, 127 insertions(+), 94 deletions(-) diff --git a/src/main/java/com/attacktimer/AnimationData.java b/src/main/java/com/attacktimer/AnimationData.java index a8b0a9c..56ac5b4 100644 --- a/src/main/java/com/attacktimer/AnimationData.java +++ b/src/main/java/com/attacktimer/AnimationData.java @@ -28,6 +28,7 @@ */ import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.security.InvalidParameterException; import java.util.Arrays; import java.util.HashMap; @@ -52,7 +53,7 @@ public enum AnimationData MELEE_LANCE_SLASH(AnimationID.HUMAN_DHUNTER_LANCE_SLASH, AttackStyle.MELEE), MELEE_FANG_STAB(AnimationID.HUMAN_OSMUMTENS_FANG, AttackStyle.MELEE), // tested w/ fang - MELEE_FANG_SPEC(AnimationID.OLAF2_BRINE_SABRE_SPECIAL, AttackStyle.MELEE, true), // tested w/ fang spec + MELEE_FANG_SPEC(AnimationID.OLAF2_BRINE_SABRE_SPECIAL, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested w/ fang spec MELEE_GENERIC_SLASH(AnimationID.HUMAN_AXE_CHOP, AttackStyle.MELEE), // tested w/ zuriel's staff, d long slash, dclaws regular slash @@ -68,65 +69,65 @@ public enum AnimationData MELEE_STAFF_STAB(AnimationID.HUMAN_SPEAR_SPIKE, AttackStyle.MELEE), // tested w/ SOTD/SOL jab, vesta's spear stab, c hally MELEE_SPEAR_CRUSH(AnimationID.HUMAN_SPEAR_LUNGE, AttackStyle.MELEE), // tested w/ vesta's spear MELEE_STAFF_SLASH(AnimationID.HUMAN_SCYTHE_SWEEP, AttackStyle.MELEE), // tested w/ SOTD/SOL slash, zammy hasta slash, vesta's spear slash, c hally - MELEE_DLONG_SPEC(AnimationID.CLEAVE, AttackStyle.MELEE, true), // tested w/ d long spec, also thammaron's sceptre crush (????)... - MELEE_DRAGON_MACE_SPEC(AnimationID.SHATTER, AttackStyle.MELEE, true), - MELEE_DRAGON_DAGGER_SPEC(AnimationID.PUNCTURE, AttackStyle.MELEE, true), - MELEE_DRAGON_WARHAMMER_SPEC(AnimationID.DRAGON_WARHAMMER_SA_PLAYER, AttackStyle.MELEE, true), // tested w/ dwh, statius warhammer spec + MELEE_DLONG_SPEC(AnimationID.CLEAVE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested w/ d long spec, also thammaron's sceptre crush (????)... + MELEE_DRAGON_MACE_SPEC(AnimationID.SHATTER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), + MELEE_DRAGON_DAGGER_SPEC(AnimationID.PUNCTURE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), + MELEE_DRAGON_WARHAMMER_SPEC(AnimationID.DRAGON_WARHAMMER_SA_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested w/ dwh, statius warhammer spec MELEE_ABYSSAL_WHIP(AnimationID.SLAYER_ABYSSAL_WHIP_ATTACK, AttackStyle.MELEE), // tested w/ whip, tent whip MELEE_GRANITE_MAUL(AnimationID.SLAYER_GRANITE_MAUL_ATTACK, AttackStyle.MELEE), // tested w/ normal gmaul, ornate maul - MELEE_GRANITE_MAUL_SPEC(AnimationID.SLAYER_GRANITE_MAUL_SPECIAL_ATTACK, AttackStyle.MELEE, true), // tested w/ normal gmaul, ornate maul + MELEE_GRANITE_MAUL_SPEC(AnimationID.SLAYER_GRANITE_MAUL_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested w/ normal gmaul, ornate maul MELEE_DHAROKS_GREATAXE_CRUSH(AnimationID.BARROW_DHAROK_SLASH, AttackStyle.MELEE), MELEE_DHAROKS_GREATAXE_SLASH(AnimationID.BARROW_DHAROK_CRUSH, AttackStyle.MELEE), MELEE_AHRIMS_STAFF_CRUSH(AnimationID.BARROWS_QUARTERSTAFF_ATTACK, AttackStyle.MELEE), MELEE_OBBY_MAUL_CRUSH(AnimationID.DRAGON_PICKAXE_ANIM, AttackStyle.MELEE), MELEE_ABYSSAL_DAGGER_STAB(AnimationID.ABYSSAL_DAGGER_LUNGE, AttackStyle.MELEE), // spec un-tested MELEE_ABYSSAL_BLUDGEON_CRUSH(AnimationID.ABYSSAL_BLUDGEON_CRUSH, AttackStyle.MELEE), - MELEE_ABYSSAL_BLUDGEON_SPEC(AnimationID.ABYSSAL_BLUDGEON_SPECIAL_ATTACK, AttackStyle.MELEE, true), + MELEE_ABYSSAL_BLUDGEON_SPEC(AnimationID.ABYSSAL_BLUDGEON_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), MELEE_LEAF_BLADED_BATTLEAXE_CRUSH(AnimationID.BATTLEAXE_CRUSH, AttackStyle.MELEE), MELEE_INQUISITORS_MACE(AnimationID.HUMAN_INQUISITORS_MACE_CRUSH, AttackStyle.MELEE), MELEE_BARRELCHEST_ANCHOR_CRUSH(AnimationID.BRAIN_PLAYER_ANCHOR_ATTACK, AttackStyle.MELEE), - MELEE_BARRELCHEST_ANCHOR_CRUSH_SPEC(AnimationID.BRAIN_PLAYER_ANCHOR_SPECIAL_ATTACK, AttackStyle.MELEE, true), + MELEE_BARRELCHEST_ANCHOR_CRUSH_SPEC(AnimationID.BRAIN_PLAYER_ANCHOR_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), MELEE_LEAF_BLADED_BATTLEAXE_SLASH(AnimationID.GODWARS_GODSWORD_ZAMORAK_PLAYER, AttackStyle.MELEE), MELEE_GODSWORD_SLASH(AnimationID.DH_SWORD_UPDATE_SLASH, AttackStyle.MELEE), // tested w/ AGS, BGS, ZGS, SGS, AGS(or) sara sword MELEE_GODSWORD_CRUSH(AnimationID.DH_SWORD_UPDATE_SMASH, AttackStyle.MELEE), // tested w/ AGS, BGS, ZGS, SGS, sara sword MELEE_GODSWORD_DEFENSIVE(AnimationID.DH_SWORD_UPDATE_BLOCK, AttackStyle.MELEE), // tested w/ BGS - MELEE_RUNE_CLAWS_SPEC(AnimationID.IMPALE, AttackStyle.MELEE, true), - MELEE_DRAGON_CLAWS_SPEC(AnimationID.HUMAN_DRAGON_CLAWS_SPEC, AttackStyle.MELEE, true), - MELEE_VLS_SPEC(AnimationID.HUMAN_DRAGON_SWORD_SPEC, AttackStyle.MELEE, true), // both VLS and dragon sword spec + MELEE_RUNE_CLAWS_SPEC(AnimationID.IMPALE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), + MELEE_DRAGON_CLAWS_SPEC(AnimationID.HUMAN_DRAGON_CLAWS_SPEC, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), + MELEE_VLS_SPEC(AnimationID.HUMAN_DRAGON_SWORD_SPEC, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // both VLS and dragon sword spec MELEE_ELDER_MAUL(AnimationID.HUMAN_ELDER_MAUL_ATTACK, AttackStyle.MELEE), - MELEE_ZAMORAK_GODSWORD_SPEC(AnimationID.ZGS_SPECIAL_PLAYER, AttackStyle.MELEE, true), // tested zgs spec + MELEE_ZAMORAK_GODSWORD_SPEC(AnimationID.ZGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested zgs spec MELEE_ELDER_MAUL_SPEC(AnimationID.HUMAN_ELDER_MAUL_SPEC, AttackStyle.MELEE), - MELEE_ZAMORAK_GODSWORD_OR_SPEC(AnimationID.ZGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, true), // verified 22/06/2024, assumed due to ags(or) - MELEE_SARADOMIN_GODSWORD_SPEC(AnimationID.SGS_SPECIAL_PLAYER, AttackStyle.MELEE, true), // tested sgs spec - MELEE_SARADOMIN_GODSWORD_OR_SPEC(AnimationID.SGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, true), // verified 22/06/2024, assumed due to ags(or) - MELEE_BANDOS_GODSWORD_SPEC(AnimationID.BGS_SPECIAL_PLAYER, AttackStyle.MELEE, true), // tested bgs spec - MELEE_BANDOS_GODSWORD_OR_SPEC(AnimationID.BGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, true), // verified 22/06/2024, assumed due to ags(or) - MELEE_ARMADYL_GODSWORD_SPEC(AnimationID.AGS_SPECIAL_PLAYER, AttackStyle.MELEE, true), // tested ags spec - MELEE_ARMADYL_GODSWORD_OR_SPEC(AnimationID.AGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, true), // tested ags(or) spec + MELEE_ZAMORAK_GODSWORD_OR_SPEC(AnimationID.ZGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // verified 22/06/2024, assumed due to ags(or) + MELEE_SARADOMIN_GODSWORD_SPEC(AnimationID.SGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested sgs spec + MELEE_SARADOMIN_GODSWORD_OR_SPEC(AnimationID.SGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // verified 22/06/2024, assumed due to ags(or) + MELEE_BANDOS_GODSWORD_SPEC(AnimationID.BGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested bgs spec + MELEE_BANDOS_GODSWORD_OR_SPEC(AnimationID.BGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // verified 22/06/2024, assumed due to ags(or) + MELEE_ARMADYL_GODSWORD_SPEC(AnimationID.AGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested ags spec + MELEE_ARMADYL_GODSWORD_OR_SPEC(AnimationID.AGS_SPECIAL_ORNATE_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested ags(or) spec MELEE_SCYTHE(AnimationID.SCYTHE_OF_VITUR_ATTACK, AttackStyle.MELEE), // tested w/ all scythe styles (so could be crush, but unlikely) MELEE_GHAZI_RAPIER_STAB(AnimationID.GHRAZI_RAPIER_ATTACK, AttackStyle.MELEE), // rapier slash is 390, basic slash animation. Also VLS stab. - MELEE_ANCIENT_GODSWORD_SPEC(AnimationID.NGS_SPECIAL_PLAYER, AttackStyle.MELEE, true), - MELEE_CRYSTAL_HALBERD_SPEC(AnimationID.DRAGON_HALBERD_SPECIAL_ATTACK, AttackStyle.MELEE, true), - MELEE_SOULREAPER_AXE(AnimationID.ANCIENT_AXE_CRUSH, AttackStyle.MELEE, true), - MELEE_SOULREAPER_AXE_SPEC(AnimationID.ANCIENT_AXE_SPECIAL, AttackStyle.MELEE, true), + MELEE_ANCIENT_GODSWORD_SPEC(AnimationID.NGS_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), + MELEE_CRYSTAL_HALBERD_SPEC(AnimationID.DRAGON_HALBERD_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), + MELEE_SOULREAPER_AXE(AnimationID.ANCIENT_AXE_CRUSH, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), + MELEE_SOULREAPER_AXE_SPEC(AnimationID.ANCIENT_AXE_SPECIAL, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), MELEE_GUTHANS_LUNGE(AnimationID.BARROWS_WAR_SPEAR_STAB, AttackStyle.MELEE), MELEE_GUTHANS_SWIPE(AnimationID.BARROWS_WAR_SPEAR_SLASH, AttackStyle.MELEE), MELEE_GUTHANS_POUNDMA(AnimationID.BARROWS_WAR_SPEAR_CRUSH, AttackStyle.MELEE), MELEE_TORAG_HAMMERS(AnimationID.BARROW_TORAG_CRUSH, AttackStyle.MELEE), MELEE_VERACS_FLAIL(AnimationID.BARROW_GUTHAN_CRUSH, AttackStyle.MELEE), MELEE_BLISTERWOOD_FLAIL_CRUSH(AnimationID.IVANDIS_FLAIL_ATTACK, AttackStyle.MELEE), // blisterwood flail - MELEE_BONE_DAGGER_SPEC(AnimationID.DTTD_PLAYER_STAB_BONE_DAGGER, AttackStyle.MELEE, true), // tested with all poison variants (p, p+, p++, none) + MELEE_BONE_DAGGER_SPEC(AnimationID.DTTD_PLAYER_STAB_BONE_DAGGER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // tested with all poison variants (p, p+, p++, none) MELEE_DUAL_MACUAHUITL(AnimationID.PMOON_MACUAHUITL_CRUSH, AttackStyle.MELEE), // https://oldschool.runescape.wiki/w/Dual_macuahuitl set effect needs custom code - MELEE_BLUE_MOON_SPEAR_SPEC(AnimationID.HUMAN_ZAMORAKSPEAR_LUNGE, AttackStyle.MELEE, true), // https://oldschool.runescape.wiki/w/Blue_moon_spear + MELEE_BLUE_MOON_SPEAR_SPEC(AnimationID.HUMAN_ZAMORAKSPEAR_LUNGE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Blue_moon_spear MELEE_BLUE_MOON_SPEAR(AnimationID.HUMAN_ZAMORAKSPEAR_STAB, AttackStyle.MELEE), MELEE_DHINS(AnimationID.HUMAN_DINHS_BULWARK_BASH, AttackStyle.MELEE), // https://oldschool.runescape.wiki/w/Dinh%27s_bulwark - MELEE_URSINE_CHAINMACE_SPEC(AnimationID.HUMAN_SPECIAL02_URSINE, AttackStyle.MELEE, true), // https://oldschool.runescape.wiki/w/Ursine_chainmace#Charged - MELEE_ANCIENT_MACE_SPEC(AnimationID.SLICE_PLAYER_MACE_SPECIAL_ATTACK, AttackStyle.MELEE, true), // https://oldschool.runescape.wiki/w/Ancient_mace - MELEE_DSCIM_SPEC(AnimationID.SP_ATTACK_DRAGON_SCIMITAR, AttackStyle.MELEE, true), // https://oldschool.runescape.wiki/w/Dragon_scimitar - MELEE_D2H_SPEC(AnimationID.DRAGON_TWO_HANDED_SWORD, AttackStyle.MELEE, true), // https://oldschool.runescape.wiki/w/Dragon_2h_sword - MELEE_ARCLIGHT_SPEC(AnimationID.DARK_SPEC_PLAYER, AttackStyle.MELEE, true), // https://oldschool.runescape.wiki/w/Arclight - MELEE_SARA_SWORD_SPEC(AnimationID.SARADOMIN_SWORD_SPECIAL_PLAYER, AttackStyle.MELEE, true), // https://oldschool.runescape.wiki/w/Saradomin_sword assumed to be the same for the blessed version - MELEE_RED_KERIS_SPEC(AnimationID.TOA_KERIS_PARTISAN_SPECIAL01, AttackStyle.MELEE, true), // https://oldschool.runescape.wiki/w/Keris_partisan_of_corruption + MELEE_URSINE_CHAINMACE_SPEC(AnimationID.HUMAN_SPECIAL02_URSINE, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Ursine_chainmace#Charged + MELEE_ANCIENT_MACE_SPEC(AnimationID.SLICE_PLAYER_MACE_SPECIAL_ATTACK, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Ancient_mace + MELEE_DSCIM_SPEC(AnimationID.SP_ATTACK_DRAGON_SCIMITAR, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Dragon_scimitar + MELEE_D2H_SPEC(AnimationID.DRAGON_TWO_HANDED_SWORD, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Dragon_2h_sword + MELEE_ARCLIGHT_SPEC(AnimationID.DARK_SPEC_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Arclight + MELEE_SARA_SWORD_SPEC(AnimationID.SARADOMIN_SWORD_SPECIAL_PLAYER, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Saradomin_sword assumed to be the same for the blessed version + MELEE_RED_KERIS_SPEC(AnimationID.TOA_KERIS_PARTISAN_SPECIAL01, AttackStyle.MELEE, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Keris_partisan_of_corruption MELEE_SALAMANDER(AnimationID.HUMAN_ATTACK_SALAMANDER, AttackStyle.MELEE), // https://oldschool.runescape.wiki/w/Salamander MELEE_INFERNAL_TECPATL(AnimationID.TECPATL_STAB, AttackStyle.MELEE), // https://oldschool.runescape.wiki/w/Infernal_tecpatl MELEE_HALLOWED_FLAIL(AnimationID.HUMAN_WEAPONS_HALLOWED_FLAIL01_ATTACK01, AttackStyle.MELEE), // https://oldschool.runescape.wiki/w/Hallowed_flail @@ -135,72 +136,73 @@ public enum AnimationData // RANGED RANGED_CHINCHOMPA(AnimationID.HUMAN_CHINCHOMPA_ATTACK_PVN, AttackStyle.RANGED), - RANGED_SHORTBOW(AnimationID.HUMAN_BOW, AttackStyle.RANGED), // Confirmed same w/ 3 types of arrows, w/ maple, magic, & hunter's shortbow, craw's bow, dbow, dbow spec + RANGED_SHORTBOW(AnimationID.HUMAN_BOW, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d), // Confirmed same w/ 3 types of arrows, w/ maple, magic, & hunter's shortbow, craw's bow, dbow, dbow spec RANGED_RUNE_KNIFE_PVP(AnimationID.HUMAN_STAKE2, AttackStyle.RANGED), // 1 tick animation, has 1 tick delay between attacks. likely same for all knives. Same for morrigan's javelins, both spec & normal attack. - RANGED_MAGIC_SHORTBOW_SPEC(AnimationID.SNAPSHOT, AttackStyle.RANGED, true), + RANGED_MAGIC_SHORTBOW_SPEC(AnimationID.SNAPSHOT, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d|MetaData.SPECIAL_ATTACK.d), RANGED_CROSSBOW_PVP(AnimationID.XBOWS_HUMAN_FIRE_AND_RELOAD, AttackStyle.RANGED), // Tested RCB & ACB w/ dragonstone bolts (e) & diamond bolts (e) RANGED_BLOWPIPE(AnimationID.SNAKEBOSS_BLOWPIPE_ATTACK, AttackStyle.RANGED), // tested in PvP with all styles. Has 1 tick delay between animations in pvp. RANGED_DARTS(AnimationID.II_HUMAN_DART_THROW_PVN, AttackStyle.RANGED), // tested w/ addy darts. Seems to be constant animation but sometimes stalls and doesn't animate RANGED_BALLISTA(AnimationID.BALLISTA_ATTACK, AttackStyle.RANGED), // Tested w/ dragon javelins. - RANGED_BALLISTA_SPEC(AnimationID.BALLISTA_SPECIAL_ATTACK_PVN, AttackStyle.RANGED, true), - RANGED_RUNE_THROWNAXE_SPEC(AnimationID.CHAINHIT, AttackStyle.RANGED, true), // https://oldschool.runescape.wiki/w/Rune_thrownaxe - RANGED_DRAGON_THROWNAXE_SPEC(AnimationID.HUMAN_DRAGON_TAXE_SPEC, AttackStyle.RANGED, true), + RANGED_BALLISTA_SPEC(AnimationID.BALLISTA_SPECIAL_ATTACK_PVN, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), + RANGED_RUNE_THROWNAXE_SPEC(AnimationID.CHAINHIT, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Rune_thrownaxe + RANGED_DRAGON_THROWNAXE_SPEC(AnimationID.HUMAN_DRAGON_TAXE_SPEC, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), RANGED_RUNE_CROSSBOW(AnimationID.XBOWS_HUMAN_FIRE_AND_RELOAD_PVN, AttackStyle.RANGED), RANGED_RUNE_CROSSBOW_OR(AnimationID.HUMAN_XBOWS_LEAGUE03_ATTACK_PVN, AttackStyle.RANGED), RANGED_BALLISTA_2(AnimationID.BALLISTA_ATTACK_PVN, AttackStyle.RANGED), // tested w/ light & heavy ballista, dragon & iron javelins. RANGED_RUNE_KNIFE(AnimationID.HUMAN_STAKE2_PVN, AttackStyle.RANGED), // 1 tick animation, has 1 tick delay between attacks. Also d thrownaxe RANGED_DRAGON_KNIFE(AnimationID.HUMAN_DRAGON_KNIFE, AttackStyle.RANGED), - RANGED_DRAGON_KNIFE_SPEC(AnimationID.HUMAN_DRAGON_TKNIVES_SPEC, AttackStyle.RANGED, true), + RANGED_DRAGON_KNIFE_SPEC(AnimationID.HUMAN_DRAGON_TKNIVES_SPEC, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), RANGED_DRAGON_KNIFE_POISONED(AnimationID.HUMAN_DRAGON_KNIFE_P, AttackStyle.RANGED), // tested w/ d knife p++ - RANGED_DRAGON_KNIFE_POISONED_SPEC(AnimationID.HUMAN_DRAGON_TKNIVES_SPEC_POISON, AttackStyle.RANGED, true), + RANGED_DRAGON_KNIFE_POISONED_SPEC(AnimationID.HUMAN_DRAGON_TKNIVES_SPEC_POISON, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), RANGED_ZARYTE_CROSSBOW(AnimationID.ZCB_ATTACK_PVN, AttackStyle.RANGED), RANGED_ZARYTE_CROSSBOW_PVP(AnimationID.ZCB_ATTACK, AttackStyle.RANGED), RANGED_BLAZING_BLOWPIPE(AnimationID.SNAKEBOSS_BLOWPIPE_ATTACK_ORNAMENT, AttackStyle.RANGED), - RANGED_VENATOR_BOW(AnimationID.HUMAN_WEAPON_BOW_VENATOR01_SHOOT, AttackStyle.RANGED), + RANGED_VENATOR_BOW(AnimationID.HUMAN_WEAPON_BOW_VENATOR01_SHOOT, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d), RANGED_KARIL_CROSSBOW(AnimationID.BARROWS_REPEATING_CROSSBOW_FIRE, AttackStyle.RANGED), RANGED_ATLATL(AnimationID.HUMAN_ATLATL_ATTACK_RANGED_01, AttackStyle.RANGED), // https://oldschool.runescape.wiki/w/Eclipse_atlatl - RANGED_ATLATL_SPEC(AnimationID.HUMAN_SPECIAL_ATLATL_01, AttackStyle.RANGED, true), + RANGED_ATLATL_SPEC(AnimationID.HUMAN_SPECIAL_ATLATL_01, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), RANGED_TONALZTICS(AnimationID.HUMAN_GLAIVE_RALOS01_CHARGED_THROW, AttackStyle.RANGED), // https://oldschool.runescape.wiki/w/Tonalztics_of_ralos#Charged - RANGED_TONALZTICS_SPEC(AnimationID.HUMAN_GLAIVE_RALOS01_CHARGED_SPECIAL, AttackStyle.RANGED, true), - RANGED_WEBWEAVER_SPEC(AnimationID.HUMAN_SPECIAL01_WEBWEAVER, AttackStyle.RANGED, true), // https://oldschool.runescape.wiki/w/Webweaver_bow#Charged - RANGED_BONE_CROSSBOW_SPEC(AnimationID.DTTD_PLAYER_FIRE_BONE_CROSSBOW_PVN, AttackStyle.RANGED, true), // https://oldschool.runescape.wiki/w/Dorgeshuun_crossbow + RANGED_TONALZTICS_SPEC(AnimationID.HUMAN_GLAIVE_RALOS01_CHARGED_SPECIAL, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), + RANGED_WEBWEAVER_SPEC(AnimationID.HUMAN_SPECIAL01_WEBWEAVER, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d|MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Webweaver_bow#Charged + RANGED_BONE_CROSSBOW_SPEC(AnimationID.DTTD_PLAYER_FIRE_BONE_CROSSBOW_PVN, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Dorgeshuun_crossbow + RANGED_SCORCHING_BOW_SPEC(AnimationID.HUMAN_WEAPON_BOW_SCORCHED_01_SPEC, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d|MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Scorching_bow // MAGIC - Keep in spellbook order (staves last) then alphabetical order and oneline - MAGIC_GOD_SPELL(AnimationID.HUMAN_CASTING, AttackStyle.MAGIC, Spellbook.STANDARD), // https://oldschool.runescape.wiki/w/God_spells - MAGIC_IBAN_BLAST(AnimationID.HUMAN_CASTIBANBLAST, AttackStyle.MAGIC, Spellbook.STANDARD), - MAGIC_SLAYER_DART(AnimationID.SLAYER_MAGICDART_CAST, AttackStyle.MAGIC, Spellbook.STANDARD), // https://oldschool.runescape.wiki/w/Magic_Dart - MAGIC_STANDARD_BIND(AnimationID.HUMAN_CASTENTANGLE, AttackStyle.MAGIC, Spellbook.STANDARD), // tested w/ bind, snare, entangle - MAGIC_STANDARD_BIND_STAFF(AnimationID.HUMAN_CASTENTANGLE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD), // tested w/ bind, snare, entangle, various staves - MAGIC_STANDARD_CONFUSE(AnimationID.HUMAN_CASTCONFUSE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD), - MAGIC_STANDARD_CRUMBLE_UNDEAD(AnimationID.HUMAN_CASTCRUMBLEUNDEAD, AttackStyle.MAGIC, Spellbook.STANDARD), - MAGIC_STANDARD_CRUMBLE_UNDEAD_HOLDING_STAFF(AnimationID.HUMAN_CASTCRUMBLEUNDEAD_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD), - MAGIC_STANDARD_ENFEEBLE(AnimationID.HUMAN_CASTENFEEBLE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD), - MAGIC_STANDARD_STRIKE_BOLT_BLAST(AnimationID.HUMAN_CASTSTRIKE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD), // tested w/ bolt - MAGIC_STANDARD_STRIKE_MANUAL(AnimationID.HUMAN_CASTSTRIKE, AttackStyle.MAGIC, Spellbook.STANDARD), - MAGIC_STANDARD_STRIKE_STAFF(AnimationID.HUMAN_CASTSTRIKE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD), - MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF(AnimationID.HUMAN_CASTSTRIKE_STAFF_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD), // strike, bolt and blast (tested all spells, different weapons) - MAGIC_STANDARD_STUN(AnimationID.HUMAN_CASTSTUN_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD), - MAGIC_STANDARD_SURGE_STAFF(AnimationID.HUMAN_CAST_SURGE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD), // tested many staves - MAGIC_STANDARD_VULNERABILITY_CURSE(AnimationID.HUMAN_CASTCURSE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD), - MAGIC_STANDARD_WAVE(AnimationID.HUMAN_CASTWAVE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD), // tested w/ wave spells - MAGIC_STANDARD_WAVE_STAFF(AnimationID.HUMAN_CASTWAVE_STAFF_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD), // tested many staves - MAGIC_STANDARD_WEAKEN(AnimationID.HUMAN_CASTWEAKEN_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD), - - MAGIC_ANCIENT_MULTI_TARGET(AnimationID.ZAROS_VERTICAL_CASTING_WALKMERGE, AttackStyle.MAGIC, Spellbook.ANCIENT), // Burst & Barrage animations (tested all 8, different weapons) - MAGIC_ANCIENT_MULTI_TARGET_PVP(AnimationID.ZAROS_VERTICAL_CASTING, AttackStyle.MAGIC, Spellbook.ANCIENT), // Burst & Barrage animations (tested all 8, different weapons) - MAGIC_ANCIENT_SINGLE_TARGET(AnimationID.ZAROS_CASTING_WALKMERGE, AttackStyle.MAGIC, Spellbook.ANCIENT), // Rush & Blitz animations (tested all 8, different weapons) - MAGIC_ANCIENT_SINGLE_TARGET_PVP(AnimationID.ZAROS_CASTING, AttackStyle.MAGIC, Spellbook.ANCIENT), // Rush & Blitz animations - - MAGIC_ARCEUUS_DEMONBANE(AnimationID.HUMAN_SPELLCAST_DEMONBANE, AttackStyle.MAGIC, Spellbook.ARCEUUS), // Also greater corruption, so that may accidentally trigger a manual-cast, but that's probably fine only affects Muspah - MAGIC_ARCEUUS_GRASP(AnimationID.HUMAN_SPELLCAST_GRASP, AttackStyle.MAGIC, Spellbook.ARCEUUS), - - MAGIC_ACCURSED_SCEPTRE_SPEC(AnimationID.HUMAN_SPECIAL_ACCURSED, AttackStyle.MAGIC, true), - MAGIC_TUMEKENS_SHADOW(AnimationID.TOA_SOT_CAST_B, AttackStyle.MAGIC, false), - MAGIC_WARPED_SCEPTRE(AnimationID.POG_WARPED_SCEPTRE_ATTACK, AttackStyle.MAGIC, false), // https://oldschool.runescape.wiki/w/Warped_sceptre - MAGIC_VOLATILE_NIGHTMARE_STAFF_SPEC(AnimationID.NIGHTMARE_STAFF_SPECIAL, AttackStyle.MAGIC, true), // assume 99 mage's base damage (does not rise when boosted). - - MAGIC_EYE_OF_AYAK(AnimationID.HUMAN_EYE_OF_AYAK_NORMAL, AttackStyle.MAGIC, false), - MAGIC_EYE_OF_AYAK_SPEC(AnimationID.HUMAN_EYE_OF_AYAK_SPECIAL, AttackStyle.MAGIC, true), // https://github.com/ngraves95/attacktimer/issues/91 + MAGIC_GOD_SPELL(AnimationID.HUMAN_CASTING, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), // https://oldschool.runescape.wiki/w/God_spells + MAGIC_IBAN_BLAST(AnimationID.HUMAN_CASTIBANBLAST, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + MAGIC_SLAYER_DART(AnimationID.SLAYER_MAGICDART_CAST, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), // https://oldschool.runescape.wiki/w/Magic_Dart + MAGIC_STANDARD_BIND(AnimationID.HUMAN_CASTENTANGLE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), // tested w/ bind, snare, entangle + MAGIC_STANDARD_BIND_STAFF(AnimationID.HUMAN_CASTENTANGLE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), // tested w/ bind, snare, entangle, various staves + MAGIC_STANDARD_CONFUSE(AnimationID.HUMAN_CASTCONFUSE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + MAGIC_STANDARD_CRUMBLE_UNDEAD(AnimationID.HUMAN_CASTCRUMBLEUNDEAD, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + MAGIC_STANDARD_CRUMBLE_UNDEAD_HOLDING_STAFF(AnimationID.HUMAN_CASTCRUMBLEUNDEAD_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + MAGIC_STANDARD_ENFEEBLE(AnimationID.HUMAN_CASTENFEEBLE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + MAGIC_STANDARD_STRIKE_BOLT_BLAST(AnimationID.HUMAN_CASTSTRIKE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), // tested w/ bolt + MAGIC_STANDARD_STRIKE_MANUAL(AnimationID.HUMAN_CASTSTRIKE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + MAGIC_STANDARD_STRIKE_STAFF(AnimationID.HUMAN_CASTSTRIKE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF(AnimationID.HUMAN_CASTSTRIKE_STAFF_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), // strike, bolt and blast (tested all spells, different weapons) + MAGIC_STANDARD_STUN(AnimationID.HUMAN_CASTSTUN_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + MAGIC_STANDARD_SURGE_STAFF(AnimationID.HUMAN_CAST_SURGE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), // tested many staves + MAGIC_STANDARD_VULNERABILITY_CURSE(AnimationID.HUMAN_CASTCURSE_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + MAGIC_STANDARD_WAVE(AnimationID.HUMAN_CASTWAVE_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), // tested w/ wave spells + MAGIC_STANDARD_WAVE_STAFF(AnimationID.HUMAN_CASTWAVE_STAFF_WALKMERGE, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), // tested many staves + MAGIC_STANDARD_WEAKEN(AnimationID.HUMAN_CASTWEAKEN_STAFF, AttackStyle.MAGIC, Spellbook.STANDARD, MetaData.NO_DATA.d), + + MAGIC_ANCIENT_MULTI_TARGET(AnimationID.ZAROS_VERTICAL_CASTING_WALKMERGE, AttackStyle.MAGIC, Spellbook.ANCIENT, MetaData.NO_DATA.d), // Burst & Barrage animations (tested all 8, different weapons) + MAGIC_ANCIENT_MULTI_TARGET_PVP(AnimationID.ZAROS_VERTICAL_CASTING, AttackStyle.MAGIC, Spellbook.ANCIENT, MetaData.NO_DATA.d), // Burst & Barrage animations (tested all 8, different weapons) + MAGIC_ANCIENT_SINGLE_TARGET(AnimationID.ZAROS_CASTING_WALKMERGE, AttackStyle.MAGIC, Spellbook.ANCIENT, MetaData.NO_DATA.d), // Rush & Blitz animations (tested all 8, different weapons) + MAGIC_ANCIENT_SINGLE_TARGET_PVP(AnimationID.ZAROS_CASTING, AttackStyle.MAGIC, Spellbook.ANCIENT, MetaData.NO_DATA.d), // Rush & Blitz animations + + MAGIC_ARCEUUS_DEMONBANE(AnimationID.HUMAN_SPELLCAST_DEMONBANE, AttackStyle.MAGIC, Spellbook.ARCEUUS, MetaData.NO_DATA.d), // Also greater corruption, so that may accidentally trigger a manual-cast, but that's probably fine only affects Muspah + MAGIC_ARCEUUS_GRASP(AnimationID.HUMAN_SPELLCAST_GRASP, AttackStyle.MAGIC, Spellbook.ARCEUUS, MetaData.NO_DATA.d), + + MAGIC_ACCURSED_SCEPTRE_SPEC(AnimationID.HUMAN_SPECIAL_ACCURSED, AttackStyle.MAGIC, MetaData.SPECIAL_ATTACK.d), + MAGIC_TUMEKENS_SHADOW(AnimationID.TOA_SOT_CAST_B, AttackStyle.MAGIC, MetaData.NO_DATA.d), + MAGIC_WARPED_SCEPTRE(AnimationID.POG_WARPED_SCEPTRE_ATTACK, AttackStyle.MAGIC, MetaData.NO_DATA.d), // https://oldschool.runescape.wiki/w/Warped_sceptre + MAGIC_VOLATILE_NIGHTMARE_STAFF_SPEC(AnimationID.NIGHTMARE_STAFF_SPECIAL, AttackStyle.MAGIC, MetaData.SPECIAL_ATTACK.d), // assume 99 mage's base damage (does not rise when boosted). + + MAGIC_EYE_OF_AYAK(AnimationID.HUMAN_EYE_OF_AYAK_NORMAL, AttackStyle.MAGIC, MetaData.NO_DATA.d), + MAGIC_EYE_OF_AYAK_SPEC(AnimationID.HUMAN_EYE_OF_AYAK_SPECIAL, AttackStyle.MAGIC, MetaData.SPECIAL_ATTACK.d), // https://github.com/ngraves95/attacktimer/issues/91 // Misc MAGIC_IMBUE(AnimationID.HUMAN_CASTBONESTOBANANAS, AttackStyle.NON_ATTACK), @@ -278,13 +280,14 @@ public enum AnimationData private static final Map DATA; private static final Map> SPELL_BOOK_ANIMATIONS; private static final Map NOT_ATTACKS; + private static final Set STANDARD_BOW_ATTACKS; public final int animationId; - public final boolean isSpecial; + public final long metaData; public final AttackStyle attackStyle; private final Spellbook spellbook; - // Simple animation data constructor for all melee and range attacks + // Simple animation data constructor for all melee, range and non attacks AnimationData(int animationId, AttackStyle attackStyle) { if (attackStyle == null) @@ -293,12 +296,12 @@ public enum AnimationData } this.animationId = animationId; this.attackStyle = attackStyle; - this.isSpecial = false; + this.metaData = MetaData.NO_DATA.d; this.spellbook = null; } - // Simple animation data constructor for all melee and range attacks w/ special - AnimationData(int animationId, AttackStyle attackStyle, boolean isSpecial) + // Simple animation data constructor for all melee, range, magic attacks with extra data + AnimationData(int animationId, AttackStyle attackStyle, long data) { if (attackStyle == null) { @@ -306,12 +309,12 @@ public enum AnimationData } this.animationId = animationId; this.attackStyle = attackStyle; - this.isSpecial = isSpecial; + this.metaData = data; this.spellbook = null; } - // Simple animation data constructor for all magic attacks - AnimationData(int animationId, AttackStyle attackStyle, Spellbook book) + // constructor for all magic attacks + AnimationData(int animationId, AttackStyle attackStyle, Spellbook book, long data) { if (attackStyle == null) { @@ -319,7 +322,7 @@ public enum AnimationData } this.animationId = animationId; this.attackStyle = attackStyle; - this.isSpecial = false; + this.metaData = data; this.spellbook = book; } @@ -328,6 +331,7 @@ public enum AnimationData ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); ImmutableMap.Builder notAttacksBuilder = new ImmutableMap.Builder<>(); Map> spellBookBuilder = new HashMap<>(); + ImmutableSet.Builder standardBowBuilder = new ImmutableSet.Builder(); for (Spellbook s : Spellbook.values()) { @@ -350,11 +354,18 @@ public enum AnimationData { notAttacksBuilder.put(data.animationId, data); } + + if (MetaData.hasFlagSet(data.metaData, MetaData.STANDARD_BOW_ATTACK)) + { + standardBowBuilder.add(data.animationId); + } + // Could also build a special attack map, but currently no use case. } DATA = builder.build(); NOT_ATTACKS = notAttacksBuilder.build(); SPELL_BOOK_ANIMATIONS = spellBookBuilder; + STANDARD_BOW_ATTACKS = standardBowBuilder.build(); } public static AnimationData fromId(int animationId) @@ -419,9 +430,35 @@ public String toString() } } + public enum MetaData + { + NO_DATA(0), + SPECIAL_ATTACK(1), + STANDARD_BOW_ATTACK(2), + ; + + MetaData(int id) + { + this.d = 1 << id; + } + + public static boolean hasFlagSet(long input, MetaData data) + { + return (input & data.d) == data.d; + } + + private final long d; + + @Override + public String toString() + { + return StringUtils.capitalize(super.toString().toLowerCase()); + } + } + public boolean isStandardBowAttack() { - throw new UnsupportedOperationException("Unimplemented method 'isStandardBowAttack'"); + return STANDARD_BOW_ATTACKS.contains(Integer.valueOf(this.animationId)); } } diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java index a0bfaa8..77cf7d3 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java @@ -33,7 +33,6 @@ import com.attacktimer.VariableSpeed.VariableSpeed; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.io.ByteArrayDataOutput; import com.google.inject.Provides; import java.awt.Color; @@ -41,7 +40,6 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayDeque; import java.util.Map; -import java.util.Set; import java.util.regex.Pattern; import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; @@ -59,13 +57,11 @@ import net.runelite.api.events.StatChanged; import net.runelite.api.events.VarbitChanged; import net.runelite.api.gameval.ItemID; -import net.runelite.api.gameval.SpotanimID; import net.runelite.api.gameval.VarPlayerID; import net.runelite.api.gameval.VarbitID; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; -import net.runelite.client.game.ItemEquipmentStats; import net.runelite.client.game.ItemManager; import net.runelite.client.game.ItemStats; import net.runelite.client.game.NPCManager; diff --git a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java index 0b12b47..50b7565 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java +++ b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java @@ -39,7 +39,7 @@ @Slf4j public class MaggotKing { - private static final int MAGGOT_KING_REGION_ID = -1; + private static final int MAGGOT_KING_REGION_ID = 11645; MaggotKing() {} From 5e7d4e2fb1d405e1ed19c6b1768b63af188fa49d Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sat, 29 Aug 2026 14:44:01 +0100 Subject: [PATCH 05/16] cleanups --- .../java/com/attacktimer/AnimationData.java | 6 ++--- .../AttackTimerMetronomePlugin.java | 10 ++------- .../com/attacktimer/ClientUtils/Utils.java | 11 ++++++++++ .../attacktimer/VariableSpeed/MaggotKing.java | 22 ++++++++++++------- .../VariableSpeed/VariableSpeed.java | 1 + 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/attacktimer/AnimationData.java b/src/main/java/com/attacktimer/AnimationData.java index 56ac5b4..d1e9d9e 100644 --- a/src/main/java/com/attacktimer/AnimationData.java +++ b/src/main/java/com/attacktimer/AnimationData.java @@ -159,8 +159,8 @@ public enum AnimationData RANGED_BLAZING_BLOWPIPE(AnimationID.SNAKEBOSS_BLOWPIPE_ATTACK_ORNAMENT, AttackStyle.RANGED), RANGED_VENATOR_BOW(AnimationID.HUMAN_WEAPON_BOW_VENATOR01_SHOOT, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d), RANGED_KARIL_CROSSBOW(AnimationID.BARROWS_REPEATING_CROSSBOW_FIRE, AttackStyle.RANGED), - RANGED_ATLATL(AnimationID.HUMAN_ATLATL_ATTACK_RANGED_01, AttackStyle.RANGED), // https://oldschool.runescape.wiki/w/Eclipse_atlatl - RANGED_ATLATL_SPEC(AnimationID.HUMAN_SPECIAL_ATLATL_01, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), + RANGED_ATLATL(AnimationID.HUMAN_ATLATL_ATTACK_RANGED_01, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d), // https://oldschool.runescape.wiki/w/Eclipse_atlatl + RANGED_ATLATL_SPEC(AnimationID.HUMAN_SPECIAL_ATLATL_01, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d|MetaData.SPECIAL_ATTACK.d), RANGED_TONALZTICS(AnimationID.HUMAN_GLAIVE_RALOS01_CHARGED_THROW, AttackStyle.RANGED), // https://oldschool.runescape.wiki/w/Tonalztics_of_ralos#Charged RANGED_TONALZTICS_SPEC(AnimationID.HUMAN_GLAIVE_RALOS01_CHARGED_SPECIAL, AttackStyle.RANGED, MetaData.SPECIAL_ATTACK.d), RANGED_WEBWEAVER_SPEC(AnimationID.HUMAN_SPECIAL01_WEBWEAVER, AttackStyle.RANGED, MetaData.STANDARD_BOW_ATTACK.d|MetaData.SPECIAL_ATTACK.d), // https://oldschool.runescape.wiki/w/Webweaver_bow#Charged @@ -456,7 +456,7 @@ public String toString() } } - + // isStandardBowAttack returns true if the animation is performed by a bow https://oldschool.runescape.wiki/w/Standard_ranged_weapons public boolean isStandardBowAttack() { return STANDARD_BOW_ATTACKS.contains(Integer.valueOf(this.animationId)); diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java index 77cf7d3..dfcce8f 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java @@ -63,7 +63,6 @@ import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; import net.runelite.client.game.ItemManager; -import net.runelite.client.game.ItemStats; import net.runelite.client.game.NPCManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; @@ -325,14 +324,8 @@ private int getWeaponSpeed(int weaponId, PoweredStaves stave, AnimationData curA } isUsingMagic = false; - final ItemStats weaponStats = Utils.getWeaponStats(client, itemManager, weaponId); - if (weaponStats == null) - { - // Assume barehanded == 4t - return VariableSpeed.compute(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, spellbook, dmgDealt, specDelta, 4); - } + final int aspeed = Utils.getWeaponSpeed(client, itemManager); // Deadline for next available attack. - final int aspeed = weaponStats.getEquipment().getAspeed(); return VariableSpeed.compute(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, spellbook, dmgDealt, specDelta, aspeed); } @@ -572,6 +565,7 @@ public void onRender() attackState = AttackState.NOT_ATTACKING; } } + attackDelayHoldoffTicks = VariableSpeed.MAGGOT_KING.onRender(client, itemManager, attackDelayHoldoffTicks, config.debugLogs()); checkForLateWeaponSwaps(); } diff --git a/src/main/java/com/attacktimer/ClientUtils/Utils.java b/src/main/java/com/attacktimer/ClientUtils/Utils.java index 145a49e..397cbb2 100644 --- a/src/main/java/com/attacktimer/ClientUtils/Utils.java +++ b/src/main/java/com/attacktimer/ClientUtils/Utils.java @@ -211,4 +211,15 @@ public static ItemStats getWeaponStats(final Client client, final ItemManager it } return itemManager.getItemStats(weaponId); } + + public static int getWeaponSpeed(final Client client, final ItemManager itemManager, final int weaponId) + { + final ItemStats weaponStats = getWeaponStats(client, itemManager, weaponId); + if (weaponStats == null) + { + // Assume bare-handed + return 4; + } + return weaponStats.getEquipment().getAspeed(); + } } diff --git a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java index 50b7565..ed4377a 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java +++ b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java @@ -29,20 +29,26 @@ import com.attacktimer.AttackProcedure; import com.attacktimer.Attacking.Attacking; import com.attacktimer.ClientUtils.Utils; +import com.attacktimer.VariableSpeed.State.TickCount; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.NPC; import net.runelite.api.gameval.NpcID; import net.runelite.client.game.ItemManager; -import net.runelite.client.game.ItemStats; @Slf4j public class MaggotKing { private static final int MAGGOT_KING_REGION_ID = 11645; - MaggotKing() - {} + private TickCount tickCount; + // the tick count if a larvae was hit, this is purely to debounce + private int consumed = -1; + + MaggotKing(TickCount tc) + { + this.tickCount = tc; + } // https://oldschool.runescape.wiki/w/Maggot_King/Strategies#Ur-maggot_larvae // @@ -55,7 +61,7 @@ public class MaggotKing public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, final boolean debugLogs) { - if (!Utils.isInRegionId(client, MAGGOT_KING_REGION_ID)) + if (!Utils.isInRegionId(client, MAGGOT_KING_REGION_ID) || tickCount.isWithinNTicks(consumed, 1)) { return attackDelayHoldoffTicks; } @@ -81,12 +87,12 @@ public int onRender(final Client client, final ItemManager itemManager, final in { log.debug("MaggotKing success, attacking maggot with bow"); } + consumed = tickCount.get(); - final ItemStats weaponStats = Utils.getWeaponStats(client, itemManager, Utils.getWeaponId(client)); - final int aspeed = weaponStats.getEquipment().getAspeed(); + final int aspeed = Utils.getWeaponSpeed(client, itemManager); // We don't want a full variable speed here, we know apriori that none of them will apply (leagues // will but that's hard to test and changes every time it comes around) - return VariableSpeed.RAPID_ATTACK_STYLE.apply(client, anim, AttackProcedure.MELEE_OR_RANGE, null, -1, -1, - aspeed, aspeed); + return VariableSpeed.RAPID_ATTACK_STYLE + .apply(client, anim, AttackProcedure.MELEE_OR_RANGE, null, -1, -1, aspeed, aspeed); } } diff --git a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java index 079f1ed..762f1d9 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java +++ b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java @@ -137,4 +137,5 @@ public static void onNpcDespawned(final Client client, final NpcDespawned npcDes // Variable speed that doesn't neatly fit in to the IVariable speed pattern (it's not weapon related // but boss related). public static final ShadowCrash SHADOW_CRASH = new ShadowCrash(YAMA, MARK_OF_DARKNESS, AttackTimerMetronomePlugin.TC); + public static final MaggotKing MAGGOT_KING = new MaggotKing(AttackTimerMetronomePlugin.TC); } From b1ab5f995ea2b041c1c775aabf10c816dca2c0a7 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sat, 29 Aug 2026 22:58:10 +0100 Subject: [PATCH 06/16] doom and refactorings --- .../java/com/attacktimer/AttackSpeed.java | 178 ++++++++++++++++++ .../AttackTimerMetronomePlugin.java | 120 +++--------- src/main/java/com/attacktimer/Damage.java | 10 +- .../java/com/attacktimer/ManualCasting.java | 49 +++++ .../VariableSpeed/DoomOfMokhaiotl.java | 111 +++++++++++ .../attacktimer/VariableSpeed/MaggotKing.java | 26 +-- .../VariableSpeed/State/TickCount.java | 9 +- .../VariableSpeed/VariableSpeed.java | 3 +- .../com/attacktimer/IntegrationTests.java | 3 + .../java/com/attacktimer/RoyalTitansTest.java | 3 + .../com/attacktimer/TormentedDemonsTest.java | 3 + .../com/attacktimer/testdata/PunishTest.txt | 2 +- .../attacktimer/testdata/PunishWastedTest.txt | 2 +- .../testdata/PunishWastedWrongStyleTest.txt | 2 +- .../testdata/SingleKillManualCast.txt | 2 +- 15 files changed, 400 insertions(+), 123 deletions(-) create mode 100644 src/main/java/com/attacktimer/AttackSpeed.java create mode 100644 src/main/java/com/attacktimer/ManualCasting.java create mode 100644 src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java diff --git a/src/main/java/com/attacktimer/AttackSpeed.java b/src/main/java/com/attacktimer/AttackSpeed.java new file mode 100644 index 0000000..7c1ca91 --- /dev/null +++ b/src/main/java/com/attacktimer/AttackSpeed.java @@ -0,0 +1,178 @@ +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 com.attacktimer.ClientUtils.Utils; +import com.attacktimer.VariableSpeed.State.TickCount; +import com.attacktimer.VariableSpeed.VariableSpeed; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayDeque; +import java.util.Map; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.events.FakeXpDrop; +import net.runelite.api.events.SoundEffectPlayed; +import net.runelite.api.events.StatChanged; +import net.runelite.api.gameval.ItemID; +import net.runelite.client.game.ItemManager; + +/** + * + * AttackSpeed stores the state specifically for computing the attack speed of a player, not all state is here + * as individual variable speed implementations may also track their own state. It was refactored out of the + * main plugin so that other capabilities can compute this. + */ +public class AttackSpeed +{ + AttackSpeed(final TickCount tc) + { + this.tickCount = tc; + } + + private ArrayDeque specialPercentageEvents = new ArrayDeque(); + private final Damage damage = new Damage(); + @Getter + private int dmgDealt = -1; + private final TickCount tickCount; + @Getter + private int soundEffectTick = -1; + @Getter + private int soundEffectId = -1; + @Getter + private boolean isUsingMagic = false; + + /** + * compute determines from the current client state what attack speed the player has, this includes all + * variables, weapon speed, unique boss mechanics etc. + * + * This is centralised here because there's a large amount of complexity, (e.g. the variable speed + * implementations). + * + * This method is stateful it should only be called when the calling code is certain that a player attack + * has occurred, it sets the isUsing magic flag on call. + * + * @param client the runelite client + * @param curAnimation the animation currently being done by the player + * @param spellbook the current spellbook the player is on + * @param itemManager the runelite item manager + * @return the attack speed of the player. + */ + public int compute(final Client client, final AnimationData curAnimation, final Spellbook spellbook, final ItemManager itemManager) + { + final int weaponId = Utils.getWeaponId(client); + final PoweredStaves stave = PoweredStaves.getPoweredStaves(weaponId, curAnimation); + final var specDelta = Utils.getLastDelta(specialPercentageEvents); + dmgDealt = damage.compute(tickCount); + if (stave != null && stave.getAnimations().contains(curAnimation)) + { + isUsingMagic = true; + // We are currently dealing with a staves in which case we can make decisions based on the + // spellbook flag. We can only improve this by using a deprecated API to check the projectile + // matches the stave rather than a manual spell, but this is good enough for now. + return VariableSpeed.compute(client, curAnimation, AttackProcedure.POWERED_STAVE, spellbook, dmgDealt, specDelta, 4); + } + + if (matchesSpellbook(client, curAnimation, spellbook) + && ManualCasting.is(client, curAnimation, soundEffectTick, soundEffectId)) + { + isUsingMagic = true; + // You can cast with anything equipped in which case we shouldn't look to invent for speed. + return VariableSpeed.compute(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, spellbook, dmgDealt, specDelta, getMagicBaseSpeed(weaponId)); + } + + isUsingMagic = false; + final int aspeed = Utils.getWeaponSpeed(client, itemManager, weaponId); + // Deadline for next available attack. + return VariableSpeed.compute(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, spellbook, dmgDealt, specDelta, aspeed); + } + + public void onTick() + { + while (specialPercentageEvents.size() > 5) + { + specialPercentageEvents.removeFirst(); + } + damage.onTick(); + } + + public void varbitSpecialAttackChanged(final int value) + { + specialPercentageEvents.addLast(value); + } + + public boolean onXpDrop(final FakeXpDrop event) + { + return damage.onXpDrop(event, tickCount); + } + + public boolean onXpDrop(final StatChanged event) + { + return damage.onXpDrop(event, tickCount); + } + + public void onSoundEffectPlayed(final Client client, final SoundEffectPlayed event) + { + // event.getSource() will be null if the player cast a spell, it's only for area sounds. + soundEffectTick = client.getTickCount(); + soundEffectId = event.getSoundId(); + } + + private static final Map NON_STANDARD_MAGIC_WEAPON_SPEEDS = new ImmutableMap.Builder() + .put(ItemID.TWINFLAME_STAFF, 6).build(); + + private static int getMagicBaseSpeed(final int weaponId) + { + return NON_STANDARD_MAGIC_WEAPON_SPEEDS.getOrDefault(weaponId, 5); + } + + // matchesSpellbook tries two methods, matching the animation the spell book based on the enum of + // pre-coded matches, and then the second set of matches against the known sound id of the spell (which + // unfortunately doesn't work if the player has them disabled). + private boolean matchesSpellbook(final Client client, final AnimationData curAnimation, final Spellbook currentSpellBook) + { + if (curAnimation != null && curAnimation.matchesSpellbook(currentSpellBook)) + { + return true; + } + if (client.getTickCount() == soundEffectTick) + { + return CastingSoundData.getSpellBookFromId(soundEffectId) == currentSpellBook; + } + return false; + } + + @VisibleForTesting + public void reset() + { + dmgDealt = -1; + soundEffectId = -1; + soundEffectId = -1; + isUsingMagic = false; + specialPercentageEvents.clear(); + damage.reset(); + } +} diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java index dfcce8f..8d75337 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java @@ -32,14 +32,11 @@ import com.attacktimer.VariableSpeed.State.TickCount; import com.attacktimer.VariableSpeed.VariableSpeed; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableMap; import com.google.common.io.ByteArrayDataOutput; import com.google.inject.Provides; import java.awt.Color; import java.awt.Dimension; import java.nio.charset.StandardCharsets; -import java.util.ArrayDeque; -import java.util.Map; import java.util.regex.Pattern; import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; @@ -56,7 +53,6 @@ import net.runelite.api.events.SoundEffectPlayed; import net.runelite.api.events.StatChanged; import net.runelite.api.events.VarbitChanged; -import net.runelite.api.gameval.ItemID; import net.runelite.api.gameval.VarPlayerID; import net.runelite.api.gameval.VarbitID; import net.runelite.client.config.ConfigManager; @@ -123,26 +119,16 @@ public enum AttackState private Spellbook currentSpellBook = Spellbook.STANDARD; private int lastUsedWeaponId = -1; private Actor lastTarget = null; - private int soundEffectTick = -1; - private int soundEffectId = -1; private boolean isUsingMagic = false; public int pendingEatDelayTicks = 0; - - private ArrayDeque specialPercentageEvents = new ArrayDeque(); - private static final Damage DAMAGE = new Damage(); - private int dmgDealt = -1; - public static final TickCount TC = new TickCount(); + public static final AttackSpeed ATTACK_SPEED = new AttackSpeed(TC); private static final int UI_HIDE_DEBOUNCE_TICKS_MAX = 1; private static final int ATTACK_DELAY_NONE = 0; public static final int DEFAULT_SIZE_UNIT_PX = 25; - private static final Map NON_STANDARD_MAGIC_WEAPON_SPEEDS = new ImmutableMap.Builder() - .put(ItemID.TWINFLAME_STAFF, 6) - .build(); - // https://oldschool.runescape.wiki/w/Food/Fast_foods#Food_Delays // These constants are not to be confused with eat delay. private final int SLOW_FOOD_ATTACK_DELAY_TICKS = 4; @@ -162,7 +148,7 @@ public void onVarbitChanged(final VarbitChanged varbitChanged) } if (varbitChanged.getVarpId() == VarPlayerID.SA_ENERGY) { - specialPercentageEvents.addLast(varbitChanged.getValue()); + ATTACK_SPEED.varbitSpecialAttackChanged(varbitChanged.getValue()); } } @@ -173,9 +159,7 @@ public void onSoundEffectPlayed(final SoundEffectPlayed event) { if (!config.enableMetronome()) return; - // event.getSource() will be null if the player cast a spell, it's only for area sounds. - soundEffectTick = client.getTickCount(); - soundEffectId = event.getSoundId(); + ATTACK_SPEED.onSoundEffectPlayed(client, event); } @Subscribe @@ -183,7 +167,7 @@ protected void onFakeXpDrop(final FakeXpDrop event) { if (!config.enableMetronome()) return; - if (DAMAGE.onXpDrop(event, TC)) + if (ATTACK_SPEED.onXpDrop(event)) { if (inPreAttackWindow()) { @@ -199,7 +183,7 @@ protected void onStatChanged(final StatChanged event) { if (!config.enableMetronome()) return; - if (DAMAGE.onXpDrop(event, TC)) + if (ATTACK_SPEED.onXpDrop(event)) { if (inPreAttackWindow()) { @@ -274,74 +258,9 @@ AttackTimerMetronomeConfig provideConfig(ConfigManager configManager) private void setAttackDelay() { - int weaponId = Utils.getWeaponId(client); - AnimationData curAnimation = AnimationData.fromId(client.getLocalPlayer().getAnimation()); - PoweredStaves stave = PoweredStaves.getPoweredStaves(weaponId, curAnimation); - boolean matchesSpellbook = matchesSpellbook(curAnimation); - attackDelayHoldoffTicks = getWeaponSpeed(weaponId, stave, curAnimation, currentSpellBook, matchesSpellbook); - lastUsedWeaponId = weaponId; - } - - // matchesSpellbook tries two methods, matching the animation the spell book based on the enum of - // pre-coded matches, and then the second set of matches against the known sound id of the spell (which - // unfortunately doesn't work if the player has them disabled). - private boolean matchesSpellbook(AnimationData curAnimation) - { - if (curAnimation != null && curAnimation.matchesSpellbook(currentSpellBook)) - { - return true; - } - if (client.getTickCount() == soundEffectTick) - { - return CastingSoundData.getSpellBookFromId(soundEffectId) == currentSpellBook; - } - return false; - } - - private int getMagicBaseSpeed(int weaponId) - { - return NON_STANDARD_MAGIC_WEAPON_SPEEDS.getOrDefault(weaponId, 5); - } - - private int getWeaponSpeed(int weaponId, PoweredStaves stave, AnimationData curAnimation, Spellbook spellbook, boolean matchesSpellbook) - { - final var specDelta = Utils.getLastDelta(specialPercentageEvents); - dmgDealt = DAMAGE.compute(TC); - if (stave != null && stave.getAnimations().contains(curAnimation)) - { - isUsingMagic = true; - // We are currently dealing with a staves in which case we can make decisions based on the - // spellbook flag. We can only improve this by using a deprecated API to check the projectile - // matches the stave rather than a manual spell, but this is good enough for now. - return VariableSpeed.compute(client, curAnimation, AttackProcedure.POWERED_STAVE, spellbook, dmgDealt, specDelta, 4); - } - - if (matchesSpellbook && isManualCasting(curAnimation)) - { - isUsingMagic = true; - // You can cast with anything equipped in which case we shouldn't look to invent for speed. - return VariableSpeed.compute(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, spellbook, dmgDealt, specDelta, getMagicBaseSpeed(weaponId)); - } - - isUsingMagic = false; - final int aspeed = Utils.getWeaponSpeed(client, itemManager); - // Deadline for next available attack. - return VariableSpeed.compute(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, spellbook, dmgDealt, specDelta, aspeed); - } - - private boolean isManualCasting(AnimationData curId) - { - // If you use a weapon like a blow pipe which has an animation longer than it's cool down then cast an - // ancient attack it wont have an animation at all. We can therefore need to detect this with a list - // of sounds instead. This obviously doesn't work if the player is muted. ATM I can't think of a way - // to detect this type of attack as a cast, only sound is an indication that the player is on - // cooldown, melee attacks, etc will trigger an animation overwriting the last frame of the blowpipe's - // idle animation. - final boolean castingFromSound = client.getTickCount() == soundEffectTick - ? CastingSoundData.isCastingSound(soundEffectId) - : false; - final boolean castingFromAnimation = AnimationData.isManualCasting(curId); - return castingFromSound || castingFromAnimation; + final AnimationData curAnimation = AnimationData.fromId(client.getLocalPlayer().getAnimation()); + attackDelayHoldoffTicks = ATTACK_SPEED.compute(client, curAnimation, currentSpellBook, itemManager); + lastUsedWeaponId = Utils.getWeaponId(client); } private void performAttack() @@ -485,15 +404,10 @@ public void onGameTick(GameTick tick) // This needs to come after performAttack as it's an additive affect applyAndClearEats(); - // clamp the attackDelayHoldoffTicks at -20, this is so we correctly account for eats even when not // attacking, but don't count down forever. attackDelayHoldoffTicks = Math.max(-20, attackDelayHoldoffTicks - 1); - while (specialPercentageEvents.size() > 5) - { - specialPercentageEvents.removeFirst(); - } - DAMAGE.cleanup(); + ATTACK_SPEED.onTick(); } @Override @@ -537,14 +451,14 @@ private StringBuilder getState() sb.append("tickPeriod: "); sb.append(this.tickPeriod);sb.append(SEPARATOR); sb.append("uiHideDebounceTickCount: "); sb.append(this.uiHideDebounceTickCount);sb.append(SEPARATOR); sb.append("attackDelayHoldoffTicks: "); sb.append(this.attackDelayHoldoffTicks);sb.append(SEPARATOR); - sb.append("dmgDealt: "); sb.append(this.dmgDealt);sb.append(SEPARATOR); + sb.append("dmgDealt: "); sb.append(ATTACK_SPEED.getDmgDealt());sb.append(SEPARATOR); sb.append("attackState: "); sb.append(this.attackState);sb.append(SEPARATOR); sb.append("renderedState: "); sb.append(this.renderedState);sb.append(SEPARATOR); sb.append("lastTarget: "); sb.append(this.lastTarget == null ? "null" : this.lastTarget.getName());sb.append("\n"); sb.append("pendingEatDelayTicks: "); sb.append(this.pendingEatDelayTicks);sb.append(SEPARATOR); sb.append("currentSpellBook: "); sb.append(this.currentSpellBook);sb.append(SEPARATOR); - sb.append("soundEffectTick: "); sb.append(this.soundEffectTick);sb.append(SEPARATOR); - sb.append("soundEffectId: "); sb.append(this.soundEffectId);sb.append("\n"); + sb.append("soundEffectTick: "); sb.append(ATTACK_SPEED.getSoundEffectTick());sb.append(SEPARATOR); + sb.append("soundEffectId: "); sb.append(ATTACK_SPEED.getSoundEffectId());sb.append("\n"); // @formatter:on return sb; } @@ -565,7 +479,8 @@ public void onRender() attackState = AttackState.NOT_ATTACKING; } } - attackDelayHoldoffTicks = VariableSpeed.MAGGOT_KING.onRender(client, itemManager, attackDelayHoldoffTicks, config.debugLogs()); + attackDelayHoldoffTicks = VariableSpeed.MAGGOT_KING.onRender(client, itemManager, attackDelayHoldoffTicks, currentSpellBook, config.debugLogs()); + attackDelayHoldoffTicks = VariableSpeed.DOOM_OF_MOKHAIOTL.onRender(client, itemManager, attackDelayHoldoffTicks, currentSpellBook, config.debugLogs()); checkForLateWeaponSwaps(); } @@ -599,4 +514,11 @@ private boolean inPreAttackWindow() return attackState == AttackState.DELAYED_FIRST_TICK && renderedState != attackState; } + @VisibleForTesting + public static void reset() + { + TC.reset(); + ATTACK_SPEED.reset(); + } + } diff --git a/src/main/java/com/attacktimer/Damage.java b/src/main/java/com/attacktimer/Damage.java index 7a1054f..88fdc39 100644 --- a/src/main/java/com/attacktimer/Damage.java +++ b/src/main/java/com/attacktimer/Damage.java @@ -27,6 +27,7 @@ import com.attacktimer.ClientUtils.Utils; import com.attacktimer.VariableSpeed.State.TickCount; +import com.google.common.annotations.VisibleForTesting; import java.util.ArrayDeque; import net.runelite.api.Skill; import net.runelite.api.events.FakeXpDrop; @@ -97,7 +98,7 @@ public int compute(TickCount tc) return (int) Math.round(xp * (3.0d / 4.0d) * MODIFIER * GLOBAL_MODIFIER); } - public void cleanup() + public void onTick() { while (hpExpEarnedTickCount.size() > 5) { @@ -108,4 +109,11 @@ public void cleanup() hpExpEarned.removeFirst(); } } + + @VisibleForTesting + public void reset() + { + hpExpEarned.clear(); + hpExpEarnedTickCount.clear(); + } } diff --git a/src/main/java/com/attacktimer/ManualCasting.java b/src/main/java/com/attacktimer/ManualCasting.java new file mode 100644 index 0000000..b4e4e49 --- /dev/null +++ b/src/main/java/com/attacktimer/ManualCasting.java @@ -0,0 +1,49 @@ +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 net.runelite.api.Client; + +public class ManualCasting +{ + /** + * is returns true if the plugin believes the player is currently manually casting a spell + */ + public static boolean is(final Client client, final AnimationData curId, final int soundEffectTick, final int soundEffectId) + { + // If you use a weapon like a blow pipe which has an animation longer than it's cool down then cast an + // ancient attack it wont have an animation at all. We can therefore need to detect this with a list + // of sounds instead. This obviously doesn't work if the player is muted. ATM I can't think of a way + // to detect this type of attack as a cast, only sound is an indication that the player is on + // cooldown, melee attacks, etc will trigger an animation overwriting the last frame of the blowpipe's + // idle animation. + final boolean castingFromSound = client.getTickCount() == soundEffectTick + ? CastingSoundData.isCastingSound(soundEffectId) + : false; + final boolean castingFromAnimation = AnimationData.isManualCasting(curId); + return castingFromSound || castingFromAnimation; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java new file mode 100644 index 0000000..d3a081d --- /dev/null +++ b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java @@ -0,0 +1,111 @@ +package com.attacktimer.VariableSpeed; + +/* + * 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 com.attacktimer.AnimationData; +import com.attacktimer.AttackSpeed; +import com.attacktimer.Attacking.Attacking; +import com.attacktimer.ClientUtils.Utils; +import com.attacktimer.Spellbook; +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.gameval.ItemID; +import net.runelite.api.gameval.NpcID; +import net.runelite.client.game.ItemManager; + +@Slf4j +public class DoomOfMokhaiotl +{ + private static final int DOOM_REGION_ID = -1; + private static final Set DEMONIC_LARVAE_IDS = new ImmutableSet.Builder() + .add(NpcID.DOM_DEMONIC_ENERGY) + .add(NpcID.DOM_DEMONIC_ENERGY_GIANT_MAGE) + .add(NpcID.DOM_DEMONIC_ENERGY_GIANT_RANGE) + .add(NpcID.DOM_DEMONIC_ENERGY_MAGE) + .add(NpcID.DOM_DEMONIC_ENERGY_RANGE) + .add(NpcID.DOM_DEMONIC_ENERGY_MELEE) + .build(); + + private static final Set NO_COOLDOWN_WEAPON = new ImmutableSet.Builder() + .add(ItemID.SILVERLIGHT) + .add(ItemID.DARKLIGHT) + .add(ItemID.ARCLIGHT) + .add(ItemID.EMBERLIGHT) + .add(ItemID.BONE_CLAWS) + .add(ItemID.SCORCHING_BOW) + .add(ItemID.HOLY_WATER) + .add(ItemID.EYE_OF_AYAK) + .build(); + + + private final AttackSpeed attackSpeed; + + DoomOfMokhaiotl(final AttackSpeed attackSpeed) + { + this.attackSpeed = attackSpeed; + } + + // https://oldschool.runescape.wiki/w/Doom_of_Mokhaiotl/Strategies#Demonic_larvae + // + // They may be attacked on attack cooldown: Non-demonbane attacks incur the weapon's attack delay + // afterwards, whereas demonbane attacks and the Eye of Ayak will not incur any attack delay. + public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, final Spellbook spellbook, final boolean debugLogs) + { + if (!Utils.isInRegionId(client, DOOM_REGION_ID)) + { + return attackDelayHoldoffTicks; + } + + final var atk = Attacking.PlayerAttack(client); + final AnimationData anim = AnimationData.fromId(atk.getAnimationId()); + if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC)) + { + return attackDelayHoldoffTicks; + } + + final NPC npc = (NPC) atk.getTarget(); + if (!DEMONIC_LARVAE_IDS.contains(npc.getId())) + { + return attackDelayHoldoffTicks; + } + final int weaponId = Utils.getWeaponId(client); + final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(anim) && anim == AnimationData.MAGIC_ARCEUUS_DEMONBANE; + if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell) + { + return attackDelayHoldoffTicks; + } + + if (debugLogs) + { + log.debug("DoomOfMokhaiotl success, attacking larvae with normal weapon"); + } + + return attackSpeed.compute(client, anim, spellbook, itemManager); + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java index ed4377a..4507843 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java +++ b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java @@ -26,9 +26,10 @@ */ import com.attacktimer.AnimationData; -import com.attacktimer.AttackProcedure; +import com.attacktimer.AttackSpeed; import com.attacktimer.Attacking.Attacking; import com.attacktimer.ClientUtils.Utils; +import com.attacktimer.Spellbook; import com.attacktimer.VariableSpeed.State.TickCount; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; @@ -41,13 +42,15 @@ public class MaggotKing { private static final int MAGGOT_KING_REGION_ID = 11645; - private TickCount tickCount; + private final TickCount tickCount; + private final AttackSpeed attackSpeed; // the tick count if a larvae was hit, this is purely to debounce private int consumed = -1; - MaggotKing(TickCount tc) + MaggotKing(final TickCount tc, final AttackSpeed attackSpeed) { this.tickCount = tc; + this.attackSpeed = attackSpeed; } // https://oldschool.runescape.wiki/w/Maggot_King/Strategies#Ur-maggot_larvae @@ -58,8 +61,7 @@ public class MaggotKing // Therefore this method returns `attackDelayHoldoffTicks` in all cases where this condition isn't met. // But if the condition is met this method returns a brand new number which is the attack speed of the bow // used. This number can be the same as the current delay and that's ok. - public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, - final boolean debugLogs) + public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, final Spellbook spellbook, final boolean debugLogs) { if (!Utils.isInRegionId(client, MAGGOT_KING_REGION_ID) || tickCount.isWithinNTicks(consumed, 1)) { @@ -74,12 +76,7 @@ public int onRender(final Client client, final ItemManager itemManager, final in } final NPC npc = (NPC) atk.getTarget(); - if (npc.getId() != NpcID.UR_MAGGOT_LARVAE) - { - return attackDelayHoldoffTicks; - } - - if (!anim.isStandardBowAttack()) + if (npc.getId() != NpcID.UR_MAGGOT_LARVAE || !anim.isStandardBowAttack()) { return attackDelayHoldoffTicks; } @@ -88,11 +85,6 @@ public int onRender(final Client client, final ItemManager itemManager, final in log.debug("MaggotKing success, attacking maggot with bow"); } consumed = tickCount.get(); - - final int aspeed = Utils.getWeaponSpeed(client, itemManager); - // We don't want a full variable speed here, we know apriori that none of them will apply (leagues - // will but that's hard to test and changes every time it comes around) - return VariableSpeed.RAPID_ATTACK_STYLE - .apply(client, anim, AttackProcedure.MELEE_OR_RANGE, null, -1, -1, aspeed, aspeed); + return attackSpeed.compute(client, anim, spellbook, itemManager); } } diff --git a/src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java b/src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java index ee8f0ca..beeb82b 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java +++ b/src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java @@ -25,6 +25,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import com.google.common.annotations.VisibleForTesting; import net.runelite.api.Client; import net.runelite.api.events.GameTick; @@ -33,7 +34,7 @@ */ public class TickCount implements IStateTracker { - private int tickCount; + private int tickCount = 0; public int get() { @@ -58,4 +59,10 @@ public boolean isWithinNTicks(int toCheckAgainst, int N) { return this.tickCount <= toCheckAgainst + N && this.tickCount >= toCheckAgainst; } + + @VisibleForTesting + public void reset() + { + tickCount = 0; + } } diff --git a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java index 762f1d9..f0803a7 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java +++ b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java @@ -137,5 +137,6 @@ public static void onNpcDespawned(final Client client, final NpcDespawned npcDes // Variable speed that doesn't neatly fit in to the IVariable speed pattern (it's not weapon related // but boss related). public static final ShadowCrash SHADOW_CRASH = new ShadowCrash(YAMA, MARK_OF_DARKNESS, AttackTimerMetronomePlugin.TC); - public static final MaggotKing MAGGOT_KING = new MaggotKing(AttackTimerMetronomePlugin.TC); + public static final MaggotKing MAGGOT_KING = new MaggotKing(AttackTimerMetronomePlugin.TC, AttackTimerMetronomePlugin.ATTACK_SPEED); + public static final DoomOfMokhaiotl DOOM_OF_MOKHAIOTL = new DoomOfMokhaiotl(AttackTimerMetronomePlugin.ATTACK_SPEED); } diff --git a/src/test/java/com/attacktimer/IntegrationTests.java b/src/test/java/com/attacktimer/IntegrationTests.java index 6fb3bd5..95dbf0c 100644 --- a/src/test/java/com/attacktimer/IntegrationTests.java +++ b/src/test/java/com/attacktimer/IntegrationTests.java @@ -146,6 +146,9 @@ public Player pluginMockSetup() throws Exception when(mockedClient.getEnum(EnumID.WEAPON_STYLES)).thenReturn(mockedWeaponEnum); when(mockedWeaponEnum.getIntValue(0)).thenReturn(-1); // blue-moon-spear mock + // reset stale static state from any other tests - the plugin basically has a singleton design + AttackTimerMetronomePlugin.reset(); + // Finally turn the plugin "on" underTest.startUp(); return mockedPlayer; diff --git a/src/test/java/com/attacktimer/RoyalTitansTest.java b/src/test/java/com/attacktimer/RoyalTitansTest.java index 9c97cc3..f96d257 100644 --- a/src/test/java/com/attacktimer/RoyalTitansTest.java +++ b/src/test/java/com/attacktimer/RoyalTitansTest.java @@ -284,6 +284,9 @@ public Player pluginMockSetup() throws Exception when(mockedClient.getWorldView(0)).thenReturn(mockedWorldView); when(mockedWorldView.getPlane()).thenReturn(mockedPlane); + // reset stale static state from any other tests - the plugin basically has a singleton design + AttackTimerMetronomePlugin.reset(); + // Finally turn the plugin "on" underTest.startUp(); diff --git a/src/test/java/com/attacktimer/TormentedDemonsTest.java b/src/test/java/com/attacktimer/TormentedDemonsTest.java index 7daff31..efee8a0 100644 --- a/src/test/java/com/attacktimer/TormentedDemonsTest.java +++ b/src/test/java/com/attacktimer/TormentedDemonsTest.java @@ -132,6 +132,9 @@ public Player pluginMockSetup() throws Exception when(mockedWorldView.getPlane()).thenReturn(mockedPlane); // -- NPCs + // reset stale static state from any other tests - the plugin basically has a singleton design + AttackTimerMetronomePlugin.reset(); + // Finally turn the plugin "on" underTest.startUp(); diff --git a/src/test/java/com/attacktimer/testdata/PunishTest.txt b/src/test/java/com/attacktimer/testdata/PunishTest.txt index a0a3f88..e4ac33d 100644 --- a/src/test/java/com/attacktimer/testdata/PunishTest.txt +++ b/src/test/java/com/attacktimer/testdata/PunishTest.txt @@ -2,5 +2,5 @@ tickPeriod: 0, uiHideDebounceTickCount: 0, attackDelayHoldoffTicks: 0, dmgDealt: pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 tickPeriod: 0, uiHideDebounceTickCount: -1, attackDelayHoldoffTicks: -1, dmgDealt: -1, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, lastTarget: null pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 -tickPeriod: 4, uiHideDebounceTickCount: 1, attackDelayHoldoffTicks: 3, dmgDealt: 0, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, lastTarget: null +tickPeriod: 4, uiHideDebounceTickCount: 1, attackDelayHoldoffTicks: 3, dmgDealt: -1, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, lastTarget: null pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 diff --git a/src/test/java/com/attacktimer/testdata/PunishWastedTest.txt b/src/test/java/com/attacktimer/testdata/PunishWastedTest.txt index a0a3f88..e4ac33d 100644 --- a/src/test/java/com/attacktimer/testdata/PunishWastedTest.txt +++ b/src/test/java/com/attacktimer/testdata/PunishWastedTest.txt @@ -2,5 +2,5 @@ tickPeriod: 0, uiHideDebounceTickCount: 0, attackDelayHoldoffTicks: 0, dmgDealt: pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 tickPeriod: 0, uiHideDebounceTickCount: -1, attackDelayHoldoffTicks: -1, dmgDealt: -1, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, lastTarget: null pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 -tickPeriod: 4, uiHideDebounceTickCount: 1, attackDelayHoldoffTicks: 3, dmgDealt: 0, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, lastTarget: null +tickPeriod: 4, uiHideDebounceTickCount: 1, attackDelayHoldoffTicks: 3, dmgDealt: -1, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, lastTarget: null pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 diff --git a/src/test/java/com/attacktimer/testdata/PunishWastedWrongStyleTest.txt b/src/test/java/com/attacktimer/testdata/PunishWastedWrongStyleTest.txt index 60d5438..047348d 100644 --- a/src/test/java/com/attacktimer/testdata/PunishWastedWrongStyleTest.txt +++ b/src/test/java/com/attacktimer/testdata/PunishWastedWrongStyleTest.txt @@ -2,5 +2,5 @@ tickPeriod: 0, uiHideDebounceTickCount: 0, attackDelayHoldoffTicks: 0, dmgDealt: pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 tickPeriod: 0, uiHideDebounceTickCount: -1, attackDelayHoldoffTicks: -1, dmgDealt: -1, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, lastTarget: null pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 -tickPeriod: 8, uiHideDebounceTickCount: 1, attackDelayHoldoffTicks: 7, dmgDealt: 0, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, lastTarget: null +tickPeriod: 8, uiHideDebounceTickCount: 1, attackDelayHoldoffTicks: 7, dmgDealt: -1, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, lastTarget: null pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 diff --git a/src/test/java/com/attacktimer/testdata/SingleKillManualCast.txt b/src/test/java/com/attacktimer/testdata/SingleKillManualCast.txt index 758fdc1..5c1f1dd 100644 --- a/src/test/java/com/attacktimer/testdata/SingleKillManualCast.txt +++ b/src/test/java/com/attacktimer/testdata/SingleKillManualCast.txt @@ -1,4 +1,4 @@ -tickPeriod: 0, uiHideDebounceTickCount: 0, attackDelayHoldoffTicks: 0, dmgDealt: -1, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, lastTarget: null +tickPeriod: 0, uiHideDebounceTickCount: 0, attackDelayHoldoffTicks: 0, dmgDealt: 45, attackState: NOT_ATTACKING, renderedState: NOT_ATTACKING, lastTarget: null pendingEatDelayTicks: 0, currentSpellBook: STANDARD, soundEffectTick: -1, soundEffectId: -1 [TEST MESSAGE] distance 1 tickPeriod: 4, uiHideDebounceTickCount: 1, attackDelayHoldoffTicks: 3, dmgDealt: 45, attackState: DELAYED_FIRST_TICK, renderedState: NOT_ATTACKING, lastTarget: null From 6f79d4d122b627c283535f9657885fa89c9295c0 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sat, 29 Aug 2026 23:41:20 +0100 Subject: [PATCH 07/16] version bump --- runelite-plugin.properties | 2 +- .../com/attacktimer/AttackTimerBarOverlay.java | 5 ----- .../attacktimer/AttackTimerMetronomeConfig.java | 16 +++++++++++++++- .../attacktimer/AttackTimerMetronomePlugin.java | 16 ---------------- .../java/com/attacktimer/IntegrationTests.java | 2 -- .../java/com/attacktimer/RoyalTitansTest.java | 2 -- .../com/attacktimer/TormentedDemonsTest.java | 2 -- 7 files changed, 16 insertions(+), 29 deletions(-) diff --git a/runelite-plugin.properties b/runelite-plugin.properties index 2ff5541..8852aca 100644 --- a/runelite-plugin.properties +++ b/runelite-plugin.properties @@ -1,7 +1,7 @@ displayName=AttackTimer author=ngraves95,Lexer747 build=standard -version=1.2.12 +version=1.2.13 description=A plugin to countdown until your next attack tags=pvm,timer,attack,combat,weapon plugins=com.attacktimer.AttackTimerMetronomePlugin \ No newline at end of file diff --git a/src/main/java/com/attacktimer/AttackTimerBarOverlay.java b/src/main/java/com/attacktimer/AttackTimerBarOverlay.java index af8c117..26832ba 100644 --- a/src/main/java/com/attacktimer/AttackTimerBarOverlay.java +++ b/src/main/java/com/attacktimer/AttackTimerBarOverlay.java @@ -163,11 +163,6 @@ private void onTick() { shouldShowBar = true; - if (!config.enableMetronome()) - { - shouldShowBar = false; - } - if (!config.showBar()) { shouldShowBar = false; diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java b/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java index 4d2153a..6508195 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java @@ -39,11 +39,16 @@ @ConfigGroup("attacktimermetronome") public interface AttackTimerMetronomeConfig extends Config { + /** + * @deprecated + * @return + */ @ConfigItem( position = 0, keyName = "enableMetronome", name = "Attack Timer Metronome", - description = "Enable visual metronome" + description = "Enable visual metronome", + hidden = true // deprecated ) default boolean enableMetronome() { @@ -267,6 +272,15 @@ default boolean debugLogs() return false; } + @ConfigItem( + position = 10000, + keyName = "attacktimerVersion", + name = "Plugin version: v1.2.13", + description = "" + ) + default void version() + {} + @Getter @AllArgsConstructor enum TicksPosition diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java index 8d75337..1f26ee8 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java @@ -157,16 +157,12 @@ public void onVarbitChanged(final VarbitChanged varbitChanged) @Subscribe public void onSoundEffectPlayed(final SoundEffectPlayed event) { - if (!config.enableMetronome()) - return; ATTACK_SPEED.onSoundEffectPlayed(client, event); } @Subscribe protected void onFakeXpDrop(final FakeXpDrop event) { - if (!config.enableMetronome()) - return; if (ATTACK_SPEED.onXpDrop(event)) { if (inPreAttackWindow()) @@ -181,8 +177,6 @@ protected void onFakeXpDrop(final FakeXpDrop event) @Subscribe protected void onStatChanged(final StatChanged event) { - if (!config.enableMetronome()) - return; if (ATTACK_SPEED.onXpDrop(event)) { if (inPreAttackWindow()) @@ -197,16 +191,12 @@ protected void onStatChanged(final StatChanged event) @Subscribe public void onNpcSpawned(final NpcSpawned npcSpawned) { - if (!config.enableMetronome()) - return; VariableSpeed.onNpcSpawned(client, npcSpawned); }; @Subscribe public void onNpcDespawned(final NpcDespawned npcDespawned) { - if (!config.enableMetronome()) - return; VariableSpeed.onNpcDespawned(client, npcDespawned); }; @@ -222,8 +212,6 @@ public void onConfigChanged(ConfigChanged event) @Subscribe public void onChatMessage(final ChatMessage event) { - if (!config.enableMetronome()) - return; final String message = event.getMessage(); if (EAT_MESSAGE.matcher(message).find()) @@ -322,8 +310,6 @@ public boolean isAttackCooldownPending() @Subscribe public void onInteractingChanged(InteractingChanged interactingChanged) { - if (!config.enableMetronome()) - return; Actor source = interactingChanged.getSource(); Actor target = interactingChanged.getTarget(); @@ -364,8 +350,6 @@ private void applyAndClearEats() @Subscribe public void onGameTick(GameTick tick) { - if (!config.enableMetronome()) - return; VariableSpeed.onGameTick(client, tick); final boolean isAttacking = Attacking.isPlayerAttacking(client, npcManager); switch (attackState) diff --git a/src/test/java/com/attacktimer/IntegrationTests.java b/src/test/java/com/attacktimer/IntegrationTests.java index 95dbf0c..e93f901 100644 --- a/src/test/java/com/attacktimer/IntegrationTests.java +++ b/src/test/java/com/attacktimer/IntegrationTests.java @@ -107,8 +107,6 @@ public void setup() public Player pluginMockSetup() throws Exception { - // enable the plugin - when(mockedConfig.enableMetronome()).thenReturn(true); // Create player Player mockedPlayer = mock(Player.class); when(mockedPlayer.getAnimation()).thenReturn(NO_ANIMATION); diff --git a/src/test/java/com/attacktimer/RoyalTitansTest.java b/src/test/java/com/attacktimer/RoyalTitansTest.java index f96d257..618618a 100644 --- a/src/test/java/com/attacktimer/RoyalTitansTest.java +++ b/src/test/java/com/attacktimer/RoyalTitansTest.java @@ -263,8 +263,6 @@ private void runAoETest(ByteArrayDataOutput channel, int distance, int expected, @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); diff --git a/src/test/java/com/attacktimer/TormentedDemonsTest.java b/src/test/java/com/attacktimer/TormentedDemonsTest.java index efee8a0..c2a7a69 100644 --- a/src/test/java/com/attacktimer/TormentedDemonsTest.java +++ b/src/test/java/com/attacktimer/TormentedDemonsTest.java @@ -105,8 +105,6 @@ private void runTest(String testName, int aspeed, int EQUIPPED_WEAPON_TYPE, int @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); From 891b6cb609cce70eed53f1b1c2b390e38cf5d9f8 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sat, 29 Aug 2026 23:41:29 +0100 Subject: [PATCH 08/16] only when flying --- src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java index 4507843..61b8f7f 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java +++ b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java @@ -34,6 +34,7 @@ import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.NPC; +import net.runelite.api.gameval.AnimationID; import net.runelite.api.gameval.NpcID; import net.runelite.client.game.ItemManager; @@ -76,13 +77,13 @@ public int onRender(final Client client, final ItemManager itemManager, final in } final NPC npc = (NPC) atk.getTarget(); - if (npc.getId() != NpcID.UR_MAGGOT_LARVAE || !anim.isStandardBowAttack()) + if (npc.getId() != NpcID.UR_MAGGOT_LARVAE || !anim.isStandardBowAttack() || npc.getAnimation() != AnimationID.UR_MAGGOT_LARVAE_FLY) { return attackDelayHoldoffTicks; } if (debugLogs) { - log.debug("MaggotKing success, attacking maggot with bow"); + log.debug("MaggotKing success, attacking flying maggot with bow"); } consumed = tickCount.get(); return attackSpeed.compute(client, anim, spellbook, itemManager); From f5e3e39314707a619ea57a4d6be398ae411df268 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sun, 30 Aug 2026 00:53:53 +0100 Subject: [PATCH 09/16] doom improvements --- .../com/attacktimer/ClientUtils/Utils.java | 30 ++++- .../VariableSpeed/DoomOfMokhaiotl.java | 103 +++++++++++++++--- .../VariableSpeed/VariableSpeed.java | 12 +- 3 files changed, 123 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/attacktimer/ClientUtils/Utils.java b/src/main/java/com/attacktimer/ClientUtils/Utils.java index 397cbb2..25c3441 100644 --- a/src/main/java/com/attacktimer/ClientUtils/Utils.java +++ b/src/main/java/com/attacktimer/ClientUtils/Utils.java @@ -32,6 +32,7 @@ import com.google.common.collect.ImmutableMap; import java.util.ArrayDeque; import java.util.Map; +import java.util.Set; import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.EquipmentInventorySlot; @@ -145,19 +146,40 @@ public static NPC getTargetNPC(final Client client) // returns true if the client is in the region specified by the id public static boolean isInRegionId(final Client client, final int id) { - final WorldView wv = client.getTopLevelWorldView(); - if (wv == null) + final int[] regions = regions(client); + if (regions == null || regions.length == 0) { return false; } - final int[] regions = wv.getMapRegions(); + return ArrayUtils.contains(regions, id); + } + // returns true if the client is in the region specified by the id + public static boolean isInRegionId(final Client client, final Set ids) + { + final int[] regions = regions(client); if (regions == null || regions.length == 0) { return false; } - return ArrayUtils.contains(regions, id); + for (final int id : regions) + { + if (ids.contains(id)) + { + return true; + } + } + return false; + } + private static int[] regions(final Client client) + { + final WorldView wv = client.getTopLevelWorldView(); + if (wv == null) + { + return null; + } + return wv.getMapRegions(); } // getLastDelta gets the last two elements and returns the delta between the two items. It does not modify diff --git a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java index d3a081d..16591e5 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java +++ b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java @@ -26,23 +26,31 @@ */ import com.attacktimer.AnimationData; +import com.attacktimer.AttackProcedure; import com.attacktimer.AttackSpeed; import com.attacktimer.Attacking.Attacking; import com.attacktimer.ClientUtils.Utils; import com.attacktimer.Spellbook; +import com.attacktimer.VariableSpeed.State.TickCount; import com.google.common.collect.ImmutableSet; import java.util.Set; import lombok.extern.slf4j.Slf4j; import net.runelite.api.Client; import net.runelite.api.NPC; +import net.runelite.api.gameval.AnimationID; import net.runelite.api.gameval.ItemID; import net.runelite.api.gameval.NpcID; import net.runelite.client.game.ItemManager; @Slf4j -public class DoomOfMokhaiotl +public class DoomOfMokhaiotl implements IVariableSpeed { - private static final int DOOM_REGION_ID = -1; + // As you delve deeper you change region + private static final Set DOOM_REGION_IDS = new ImmutableSet.Builder() + .add(5269) + .add(13668) + .add(14180) + .build(); private static final Set DEMONIC_LARVAE_IDS = new ImmutableSet.Builder() .add(NpcID.DOM_DEMONIC_ENERGY) .add(NpcID.DOM_DEMONIC_ENERGY_GIANT_MAGE) @@ -63,11 +71,15 @@ public class DoomOfMokhaiotl .add(ItemID.EYE_OF_AYAK) .build(); - + private final TickCount tickCount; private final AttackSpeed attackSpeed; + private int larvaeConsumed = -1; + private int shieldConsumed = -1; + private boolean inRegionId; - DoomOfMokhaiotl(final AttackSpeed attackSpeed) + DoomOfMokhaiotl(final TickCount tc, final AttackSpeed attackSpeed) { + this.tickCount = tc; this.attackSpeed = attackSpeed; } @@ -75,9 +87,13 @@ public class DoomOfMokhaiotl // // They may be attacked on attack cooldown: Non-demonbane attacks incur the weapon's attack delay // afterwards, whereas demonbane attacks and the Eye of Ayak will not incur any attack delay. + // + // Doom can also be attacked whilst on cooldown when he is charging his shield (melee punish only) + // + // The https://oldschool.runescape.wiki/w/Volatile_earth can also be attacked with no attack delay. public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, final Spellbook spellbook, final boolean debugLogs) { - if (!Utils.isInRegionId(client, DOOM_REGION_ID)) + if (!Utils.isInRegionId(client, DOOM_REGION_IDS)) { return attackDelayHoldoffTicks; } @@ -90,22 +106,83 @@ public int onRender(final Client client, final ItemManager itemManager, final in } final NPC npc = (NPC) atk.getTarget(); - if (!DEMONIC_LARVAE_IDS.contains(npc.getId())) + final int npcId = npc.getId(); + if (DEMONIC_LARVAE_IDS.contains(npcId)) { + if (tickCount.isWithinNTicks(larvaeConsumed, 1)) + { + return attackDelayHoldoffTicks; + } + final int weaponId = Utils.getWeaponId(client); + final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(anim) && anim == AnimationData.MAGIC_ARCEUUS_DEMONBANE; + if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell) + { + return attackDelayHoldoffTicks; + } + + if (debugLogs) + { + log.debug("DoomOfMokhaiotl success, attacking larvae with normal weapon"); + } + larvaeConsumed = tickCount.get(); + return attackSpeed.compute(client, anim, spellbook, itemManager); + } + else if (npcId == NpcID.DOM_BOSS) + { + if (tickCount.isWithinNTicks(shieldConsumed, 30)) + { + return attackDelayHoldoffTicks; + } + final var animId = npc.getAnimation(); + // undocumented in the wiki but from my testing these can be hit while on cooldown but unlike the + // grubs do add up the delay + if (animId == AnimationID.DOM_BEAM_CHARGE_LOOP || animId == AnimationID.DOM_BEAM_CHARGE) + { + if (Utils.getAttackType(client).IsMelee()) + { + if (debugLogs) + { + log.debug("DoomOfMokhaiotl success, on cooldown melee swing"); + } + shieldConsumed = tickCount.get(); + return attackSpeed.compute(client, anim, spellbook, itemManager) + attackDelayHoldoffTicks; + } + } return attackDelayHoldoffTicks; } - final int weaponId = Utils.getWeaponId(client); - final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(anim) && anim == AnimationData.MAGIC_ARCEUUS_DEMONBANE; - if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell) + else if (npcId == NpcID.DOM_SHOCKWAVE_PATH_NODE) { + // these never incur attack delay return attackDelayHoldoffTicks; } - - if (debugLogs) + else { - log.debug("DoomOfMokhaiotl success, attacking larvae with normal weapon"); + return attackDelayHoldoffTicks; } + } - return attackSpeed.compute(client, anim, spellbook, itemManager); + // Take care here to ensure no infinite loop or affect on the speed as the onRender does call this via variable speed + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed, + final int curSpeed) + { + final int targetId = Utils.getTargetId(client); + final boolean inDoom = Utils.isInRegionId(client, DOOM_REGION_IDS); + if (inDoom && targetId == NpcID.DOM_SHOCKWAVE_PATH_NODE) + { + log.debug("DoomOfMokhaiotl success, zero delay volatile earth"); + return 1; + } + if (inDoom && DEMONIC_LARVAE_IDS.contains(targetId)) + { + final int weaponId = Utils.getWeaponId(client); + final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(curAnimation) && curAnimation == AnimationData.MAGIC_ARCEUUS_DEMONBANE; + if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell) + { + log.debug("DoomOfMokhaiotl success, zero delay grub"); + return 1; + } + } + return curSpeed; } } diff --git a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java index f0803a7..6102a0d 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java +++ b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java @@ -107,6 +107,12 @@ public static void onNpcDespawned(final Client client, final NpcDespawned npcDes private static final MarkOfDarkness MARK_OF_DARKNESS = new MarkOfDarkness(); public static final RapidAttackStyle RAPID_ATTACK_STYLE = new RapidAttackStyle(); + // Variable speed that doesn't neatly fit in to the IVariable speed pattern (it's not weapon related + // but boss related). + public static final ShadowCrash SHADOW_CRASH = new ShadowCrash(YAMA, MARK_OF_DARKNESS, AttackTimerMetronomePlugin.TC); + public static final MaggotKing MAGGOT_KING = new MaggotKing(AttackTimerMetronomePlugin.TC, AttackTimerMetronomePlugin.ATTACK_SPEED); + public static final DoomOfMokhaiotl DOOM_OF_MOKHAIOTL = new DoomOfMokhaiotl(AttackTimerMetronomePlugin.TC, AttackTimerMetronomePlugin.ATTACK_SPEED); + private static final IStateTracker[] TO_TRACK = { // State tracking, these do not contribute themselves to any variable speed weapon/mechanic but // provide state tracking which is shared across more than one variable speed weapon/mechanic. @@ -132,11 +138,7 @@ public static void onNpcDespawned(final Client client, final NpcDespawned npcDes new Amoxliatl(), new Scurrius(), new TombsOfAmascut(), + DOOM_OF_MOKHAIOTL, }; - // Variable speed that doesn't neatly fit in to the IVariable speed pattern (it's not weapon related - // but boss related). - public static final ShadowCrash SHADOW_CRASH = new ShadowCrash(YAMA, MARK_OF_DARKNESS, AttackTimerMetronomePlugin.TC); - public static final MaggotKing MAGGOT_KING = new MaggotKing(AttackTimerMetronomePlugin.TC, AttackTimerMetronomePlugin.ATTACK_SPEED); - public static final DoomOfMokhaiotl DOOM_OF_MOKHAIOTL = new DoomOfMokhaiotl(AttackTimerMetronomePlugin.ATTACK_SPEED); } From 251215b4b690248d5d53bcda86f6cb26abf114d8 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sun, 30 Aug 2026 12:33:05 +0100 Subject: [PATCH 10/16] document 'new' mechanics --- .../VariableSpeed/DoomOfMokhaiotl.java | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java index 16591e5..17a4901 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java +++ b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java @@ -47,17 +47,22 @@ public class DoomOfMokhaiotl implements IVariableSpeed { // As you delve deeper you change region private static final Set DOOM_REGION_IDS = new ImmutableSet.Builder() - .add(5269) - .add(13668) - .add(14180) + .add(5269) // Delve 1 + .add(13668) // Delve 2 - 5 + .add(14180) // Delve 5+ .build(); - private static final Set DEMONIC_LARVAE_IDS = new ImmutableSet.Builder() + // All larvae and the volatile earth work the same: + // - if you attack them with demon bane you get no CD. + // - else non demonbane overwrites you're current CD + // - you can attack them whilst on CD. + private static final Set COOLDOWN_OVERWRITES_IDS = new ImmutableSet.Builder() .add(NpcID.DOM_DEMONIC_ENERGY) .add(NpcID.DOM_DEMONIC_ENERGY_GIANT_MAGE) .add(NpcID.DOM_DEMONIC_ENERGY_GIANT_RANGE) .add(NpcID.DOM_DEMONIC_ENERGY_MAGE) .add(NpcID.DOM_DEMONIC_ENERGY_RANGE) .add(NpcID.DOM_DEMONIC_ENERGY_MELEE) + .add(NpcID.DOM_SHOCKWAVE_PATH_NODE) .build(); private static final Set NO_COOLDOWN_WEAPON = new ImmutableSet.Builder() @@ -75,7 +80,6 @@ public class DoomOfMokhaiotl implements IVariableSpeed private final AttackSpeed attackSpeed; private int larvaeConsumed = -1; private int shieldConsumed = -1; - private boolean inRegionId; DoomOfMokhaiotl(final TickCount tc, final AttackSpeed attackSpeed) { @@ -107,7 +111,7 @@ public int onRender(final Client client, final ItemManager itemManager, final in final NPC npc = (NPC) atk.getTarget(); final int npcId = npc.getId(); - if (DEMONIC_LARVAE_IDS.contains(npcId)) + if (COOLDOWN_OVERWRITES_IDS.contains(npcId)) { if (tickCount.isWithinNTicks(larvaeConsumed, 1)) { @@ -134,8 +138,17 @@ else if (npcId == NpcID.DOM_BOSS) return attackDelayHoldoffTicks; } final var animId = npc.getAnimation(); - // undocumented in the wiki but from my testing these can be hit while on cooldown but unlike the - // grubs do add up the delay + // Undocumented in the wiki but from my testing these can be hit while on cooldown but unlike the + // grubs do add up the delay. This is original research: + // + // Atk (5) 1923 -> Chally (7) 1926 -> If plain off CD 1935 (actual: 1933 = 2 tick reduction) (3 tick gap between punish) + // Atk (5) 1945 -> Swift blade (3) 1946 -> If plain off CD 1953 (actual: 1949 = 4 tick reduction) (1 tick gap between punish) + // Atk (5) 1964 -> Rapier (4) 1966 -> If plain off CD 1973 (actual: 1970 = 3 tick reduction) (2 tick gap between punish) + // Atk (5) 2046 -> Battle axe (6) 2048 -> If plain off CD 2057 (actual: 2054 = 3 tick reduction) (2 tick gap between punish) + // Atk (5) 4317 -> Rapier (4) 4318 -> If plain off CD 4326 (actual: 4322 = 4 tick reduction) (1 tick gap between punish) + // + // Therefore my conclusion based off these samples is that resulting delay is just the attack + // delay of the weapon used, overwriting the current delay not addition. if (animId == AnimationID.DOM_BEAM_CHARGE_LOOP || animId == AnimationID.DOM_BEAM_CHARGE) { if (Utils.getAttackType(client).IsMelee()) @@ -145,16 +158,11 @@ else if (npcId == NpcID.DOM_BOSS) log.debug("DoomOfMokhaiotl success, on cooldown melee swing"); } shieldConsumed = tickCount.get(); - return attackSpeed.compute(client, anim, spellbook, itemManager) + attackDelayHoldoffTicks; + return attackSpeed.compute(client, anim, spellbook, itemManager); } } return attackDelayHoldoffTicks; } - else if (npcId == NpcID.DOM_SHOCKWAVE_PATH_NODE) - { - // these never incur attack delay - return attackDelayHoldoffTicks; - } else { return attackDelayHoldoffTicks; @@ -168,12 +176,7 @@ public int apply(final Client client, final AnimationData curAnimation, final At { final int targetId = Utils.getTargetId(client); final boolean inDoom = Utils.isInRegionId(client, DOOM_REGION_IDS); - if (inDoom && targetId == NpcID.DOM_SHOCKWAVE_PATH_NODE) - { - log.debug("DoomOfMokhaiotl success, zero delay volatile earth"); - return 1; - } - if (inDoom && DEMONIC_LARVAE_IDS.contains(targetId)) + if (inDoom && COOLDOWN_OVERWRITES_IDS.contains(targetId)) { final int weaponId = Utils.getWeaponId(client); final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(curAnimation) && curAnimation == AnimationData.MAGIC_ARCEUUS_DEMONBANE; From 34196ead327d6d3fb2d46cb0469ffe6c58248dc7 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sun, 30 Aug 2026 12:40:49 +0100 Subject: [PATCH 11/16] cleanup --- src/main/java/com/attacktimer/AnimationData.java | 12 ++++++++---- src/main/java/com/attacktimer/AttackSpeed.java | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/attacktimer/AnimationData.java b/src/main/java/com/attacktimer/AnimationData.java index d1e9d9e..82bff50 100644 --- a/src/main/java/com/attacktimer/AnimationData.java +++ b/src/main/java/com/attacktimer/AnimationData.java @@ -432,14 +432,18 @@ public String toString() public enum MetaData { - NO_DATA(0), + NO_DATA(), SPECIAL_ATTACK(1), STANDARD_BOW_ATTACK(2), ; - MetaData(int id) + MetaData(int bitshift) { - this.d = 1 << id; + this.d = 1 << bitshift; + } + MetaData() + { + this.d = 0; } public static boolean hasFlagSet(long input, MetaData data) @@ -459,6 +463,6 @@ public String toString() // isStandardBowAttack returns true if the animation is performed by a bow https://oldschool.runescape.wiki/w/Standard_ranged_weapons public boolean isStandardBowAttack() { - return STANDARD_BOW_ATTACKS.contains(Integer.valueOf(this.animationId)); + return STANDARD_BOW_ATTACKS.contains(this.animationId); } } diff --git a/src/main/java/com/attacktimer/AttackSpeed.java b/src/main/java/com/attacktimer/AttackSpeed.java index 7c1ca91..84f74cd 100644 --- a/src/main/java/com/attacktimer/AttackSpeed.java +++ b/src/main/java/com/attacktimer/AttackSpeed.java @@ -170,7 +170,7 @@ public void reset() { dmgDealt = -1; soundEffectId = -1; - soundEffectId = -1; + soundEffectTick = -1; isUsingMagic = false; specialPercentageEvents.clear(); damage.reset(); From b66df0c3aace163487e0bf1d7fdf7e0d8591ee50 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sun, 30 Aug 2026 12:43:51 +0100 Subject: [PATCH 12/16] version bump --- README.md | 8 ++++++++ runelite-plugin.properties | 2 +- .../java/com/attacktimer/AttackTimerMetronomeConfig.java | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7635302..cdd40f0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,14 @@ Ticks until next attack may be enabled over your player's head. ## Updates +## 1.3.0 + +* Support for Maggot King Larvae (overwrite attack cooldown if attacked with a bow whilst flying) +* Support for Doom of Mokhaiotl + * Larvae (overwrite attack cooldown if non-demonbane) + * Volatile Earth (overwrite attack cooldown if non-demonbane) + * Melee Punish (overwrite attack cooldown) + ## 1.2.11 - 1.2.12 * Support for Royal Titans attack cooldown speedup when killing elementals diff --git a/runelite-plugin.properties b/runelite-plugin.properties index 8852aca..cbe1868 100644 --- a/runelite-plugin.properties +++ b/runelite-plugin.properties @@ -1,7 +1,7 @@ displayName=AttackTimer author=ngraves95,Lexer747 build=standard -version=1.2.13 +version=1.3.0 description=A plugin to countdown until your next attack tags=pvm,timer,attack,combat,weapon plugins=com.attacktimer.AttackTimerMetronomePlugin \ No newline at end of file diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java b/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java index 6508195..f54a22b 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java @@ -275,7 +275,7 @@ default boolean debugLogs() @ConfigItem( position = 10000, keyName = "attacktimerVersion", - name = "Plugin version: v1.2.13", + name = "Plugin version: v1.3.0", description = "" ) default void version() From a35af95b1eb807ff0cb568a745d76f33ea871b02 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sun, 30 Aug 2026 14:07:17 +0100 Subject: [PATCH 13/16] also fix new files --- .../java/com/attacktimer/AttackSpeed.java | 356 ++++++++-------- .../com/attacktimer/Attacking/Attack.java | 92 ++--- .../com/attacktimer/Attacking/Attacking.java | 244 +++++------ .../java/com/attacktimer/ManualCasting.java | 98 ++--- .../VariableSpeed/DoomOfMokhaiotl.java | 382 +++++++++--------- .../attacktimer/VariableSpeed/MaggotKing.java | 182 ++++----- 6 files changed, 677 insertions(+), 677 deletions(-) diff --git a/src/main/java/com/attacktimer/AttackSpeed.java b/src/main/java/com/attacktimer/AttackSpeed.java index 84f74cd..fe1935b 100644 --- a/src/main/java/com/attacktimer/AttackSpeed.java +++ b/src/main/java/com/attacktimer/AttackSpeed.java @@ -1,178 +1,178 @@ -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 com.attacktimer.ClientUtils.Utils; -import com.attacktimer.VariableSpeed.State.TickCount; -import com.attacktimer.VariableSpeed.VariableSpeed; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableMap; -import java.util.ArrayDeque; -import java.util.Map; -import lombok.Getter; -import net.runelite.api.Client; -import net.runelite.api.events.FakeXpDrop; -import net.runelite.api.events.SoundEffectPlayed; -import net.runelite.api.events.StatChanged; -import net.runelite.api.gameval.ItemID; -import net.runelite.client.game.ItemManager; - -/** - * - * AttackSpeed stores the state specifically for computing the attack speed of a player, not all state is here - * as individual variable speed implementations may also track their own state. It was refactored out of the - * main plugin so that other capabilities can compute this. - */ -public class AttackSpeed -{ - AttackSpeed(final TickCount tc) - { - this.tickCount = tc; - } - - private ArrayDeque specialPercentageEvents = new ArrayDeque(); - private final Damage damage = new Damage(); - @Getter - private int dmgDealt = -1; - private final TickCount tickCount; - @Getter - private int soundEffectTick = -1; - @Getter - private int soundEffectId = -1; - @Getter - private boolean isUsingMagic = false; - - /** - * compute determines from the current client state what attack speed the player has, this includes all - * variables, weapon speed, unique boss mechanics etc. - * - * This is centralised here because there's a large amount of complexity, (e.g. the variable speed - * implementations). - * - * This method is stateful it should only be called when the calling code is certain that a player attack - * has occurred, it sets the isUsing magic flag on call. - * - * @param client the runelite client - * @param curAnimation the animation currently being done by the player - * @param spellbook the current spellbook the player is on - * @param itemManager the runelite item manager - * @return the attack speed of the player. - */ - public int compute(final Client client, final AnimationData curAnimation, final Spellbook spellbook, final ItemManager itemManager) - { - final int weaponId = Utils.getWeaponId(client); - final PoweredStaves stave = PoweredStaves.getPoweredStaves(weaponId, curAnimation); - final var specDelta = Utils.getLastDelta(specialPercentageEvents); - dmgDealt = damage.compute(tickCount); - if (stave != null && stave.getAnimations().contains(curAnimation)) - { - isUsingMagic = true; - // We are currently dealing with a staves in which case we can make decisions based on the - // spellbook flag. We can only improve this by using a deprecated API to check the projectile - // matches the stave rather than a manual spell, but this is good enough for now. - return VariableSpeed.compute(client, curAnimation, AttackProcedure.POWERED_STAVE, spellbook, dmgDealt, specDelta, 4); - } - - if (matchesSpellbook(client, curAnimation, spellbook) - && ManualCasting.is(client, curAnimation, soundEffectTick, soundEffectId)) - { - isUsingMagic = true; - // You can cast with anything equipped in which case we shouldn't look to invent for speed. - return VariableSpeed.compute(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, spellbook, dmgDealt, specDelta, getMagicBaseSpeed(weaponId)); - } - - isUsingMagic = false; - final int aspeed = Utils.getWeaponSpeed(client, itemManager, weaponId); - // Deadline for next available attack. - return VariableSpeed.compute(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, spellbook, dmgDealt, specDelta, aspeed); - } - - public void onTick() - { - while (specialPercentageEvents.size() > 5) - { - specialPercentageEvents.removeFirst(); - } - damage.onTick(); - } - - public void varbitSpecialAttackChanged(final int value) - { - specialPercentageEvents.addLast(value); - } - - public boolean onXpDrop(final FakeXpDrop event) - { - return damage.onXpDrop(event, tickCount); - } - - public boolean onXpDrop(final StatChanged event) - { - return damage.onXpDrop(event, tickCount); - } - - public void onSoundEffectPlayed(final Client client, final SoundEffectPlayed event) - { - // event.getSource() will be null if the player cast a spell, it's only for area sounds. - soundEffectTick = client.getTickCount(); - soundEffectId = event.getSoundId(); - } - - private static final Map NON_STANDARD_MAGIC_WEAPON_SPEEDS = new ImmutableMap.Builder() - .put(ItemID.TWINFLAME_STAFF, 6).build(); - - private static int getMagicBaseSpeed(final int weaponId) - { - return NON_STANDARD_MAGIC_WEAPON_SPEEDS.getOrDefault(weaponId, 5); - } - - // matchesSpellbook tries two methods, matching the animation the spell book based on the enum of - // pre-coded matches, and then the second set of matches against the known sound id of the spell (which - // unfortunately doesn't work if the player has them disabled). - private boolean matchesSpellbook(final Client client, final AnimationData curAnimation, final Spellbook currentSpellBook) - { - if (curAnimation != null && curAnimation.matchesSpellbook(currentSpellBook)) - { - return true; - } - if (client.getTickCount() == soundEffectTick) - { - return CastingSoundData.getSpellBookFromId(soundEffectId) == currentSpellBook; - } - return false; - } - - @VisibleForTesting - public void reset() - { - dmgDealt = -1; - soundEffectId = -1; - soundEffectTick = -1; - isUsingMagic = false; - specialPercentageEvents.clear(); - damage.reset(); - } -} +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 com.attacktimer.ClientUtils.Utils; +import com.attacktimer.VariableSpeed.State.TickCount; +import com.attacktimer.VariableSpeed.VariableSpeed; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import java.util.ArrayDeque; +import java.util.Map; +import lombok.Getter; +import net.runelite.api.Client; +import net.runelite.api.events.FakeXpDrop; +import net.runelite.api.events.SoundEffectPlayed; +import net.runelite.api.events.StatChanged; +import net.runelite.api.gameval.ItemID; +import net.runelite.client.game.ItemManager; + +/** + * + * AttackSpeed stores the state specifically for computing the attack speed of a player, not all state is here + * as individual variable speed implementations may also track their own state. It was refactored out of the + * main plugin so that other capabilities can compute this. + */ +public class AttackSpeed +{ + AttackSpeed(final TickCount tc) + { + this.tickCount = tc; + } + + private ArrayDeque specialPercentageEvents = new ArrayDeque(); + private final Damage damage = new Damage(); + @Getter + private int dmgDealt = -1; + private final TickCount tickCount; + @Getter + private int soundEffectTick = -1; + @Getter + private int soundEffectId = -1; + @Getter + private boolean isUsingMagic = false; + + /** + * compute determines from the current client state what attack speed the player has, this includes all + * variables, weapon speed, unique boss mechanics etc. + * + * This is centralised here because there's a large amount of complexity, (e.g. the variable speed + * implementations). + * + * This method is stateful it should only be called when the calling code is certain that a player attack + * has occurred, it sets the isUsing magic flag on call. + * + * @param client the runelite client + * @param curAnimation the animation currently being done by the player + * @param spellbook the current spellbook the player is on + * @param itemManager the runelite item manager + * @return the attack speed of the player. + */ + public int compute(final Client client, final AnimationData curAnimation, final Spellbook spellbook, final ItemManager itemManager) + { + final int weaponId = Utils.getWeaponId(client); + final PoweredStaves stave = PoweredStaves.getPoweredStaves(weaponId, curAnimation); + final var specDelta = Utils.getLastDelta(specialPercentageEvents); + dmgDealt = damage.compute(tickCount); + if (stave != null && stave.getAnimations().contains(curAnimation)) + { + isUsingMagic = true; + // We are currently dealing with a staves in which case we can make decisions based on the + // spellbook flag. We can only improve this by using a deprecated API to check the projectile + // matches the stave rather than a manual spell, but this is good enough for now. + return VariableSpeed.compute(client, curAnimation, AttackProcedure.POWERED_STAVE, spellbook, dmgDealt, specDelta, 4); + } + + if (matchesSpellbook(client, curAnimation, spellbook) + && ManualCasting.is(client, curAnimation, soundEffectTick, soundEffectId)) + { + isUsingMagic = true; + // You can cast with anything equipped in which case we shouldn't look to invent for speed. + return VariableSpeed.compute(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, spellbook, dmgDealt, specDelta, getMagicBaseSpeed(weaponId)); + } + + isUsingMagic = false; + final int aspeed = Utils.getWeaponSpeed(client, itemManager, weaponId); + // Deadline for next available attack. + return VariableSpeed.compute(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, spellbook, dmgDealt, specDelta, aspeed); + } + + public void onTick() + { + while (specialPercentageEvents.size() > 5) + { + specialPercentageEvents.removeFirst(); + } + damage.onTick(); + } + + public void varbitSpecialAttackChanged(final int value) + { + specialPercentageEvents.addLast(value); + } + + public boolean onXpDrop(final FakeXpDrop event) + { + return damage.onXpDrop(event, tickCount); + } + + public boolean onXpDrop(final StatChanged event) + { + return damage.onXpDrop(event, tickCount); + } + + public void onSoundEffectPlayed(final Client client, final SoundEffectPlayed event) + { + // event.getSource() will be null if the player cast a spell, it's only for area sounds. + soundEffectTick = client.getTickCount(); + soundEffectId = event.getSoundId(); + } + + private static final Map NON_STANDARD_MAGIC_WEAPON_SPEEDS = new ImmutableMap.Builder() + .put(ItemID.TWINFLAME_STAFF, 6).build(); + + private static int getMagicBaseSpeed(final int weaponId) + { + return NON_STANDARD_MAGIC_WEAPON_SPEEDS.getOrDefault(weaponId, 5); + } + + // matchesSpellbook tries two methods, matching the animation the spell book based on the enum of + // pre-coded matches, and then the second set of matches against the known sound id of the spell (which + // unfortunately doesn't work if the player has them disabled). + private boolean matchesSpellbook(final Client client, final AnimationData curAnimation, final Spellbook currentSpellBook) + { + if (curAnimation != null && curAnimation.matchesSpellbook(currentSpellBook)) + { + return true; + } + if (client.getTickCount() == soundEffectTick) + { + return CastingSoundData.getSpellBookFromId(soundEffectId) == currentSpellBook; + } + return false; + } + + @VisibleForTesting + public void reset() + { + dmgDealt = -1; + soundEffectId = -1; + soundEffectTick = -1; + isUsingMagic = false; + specialPercentageEvents.clear(); + damage.reset(); + } +} diff --git a/src/main/java/com/attacktimer/Attacking/Attack.java b/src/main/java/com/attacktimer/Attacking/Attack.java index 03bd7d7..aa0fb97 100644 --- a/src/main/java/com/attacktimer/Attacking/Attack.java +++ b/src/main/java/com/attacktimer/Attacking/Attack.java @@ -1,46 +1,46 @@ -package com.attacktimer.Attacking; - -/* - * 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 lombok.Getter; -import net.runelite.api.Actor; - -public final class Attack -{ - @Getter - private final int animationId; - @Getter - private final boolean salamander; - @Getter - private final Actor target; - - public Attack(final int animationId, final boolean salamander, final Actor target) - { - this.animationId = animationId; - this.salamander = salamander; - this.target = target; - } -} +package com.attacktimer.Attacking; + +/* + * 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 lombok.Getter; +import net.runelite.api.Actor; + +public final class Attack +{ + @Getter + private final int animationId; + @Getter + private final boolean salamander; + @Getter + private final Actor target; + + public Attack(final int animationId, final boolean salamander, final Actor target) + { + this.animationId = animationId; + this.salamander = salamander; + this.target = target; + } +} diff --git a/src/main/java/com/attacktimer/Attacking/Attacking.java b/src/main/java/com/attacktimer/Attacking/Attacking.java index c06d9d5..fc8a92c 100644 --- a/src/main/java/com/attacktimer/Attacking/Attacking.java +++ b/src/main/java/com/attacktimer/Attacking/Attacking.java @@ -1,122 +1,122 @@ -package com.attacktimer.Attacking; - -/* - * Copyright (c) 2022, Nick Graves - * Copyright (c) 2024-2026, Lexer747 - * Copyright (c) 2024-2026, Richardant - * 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 com.attacktimer.AnimationData; -import com.google.common.collect.ImmutableSet; -import java.util.Arrays; -import java.util.Set; -import net.runelite.api.Actor; -import net.runelite.api.Client; -import net.runelite.api.NPC; -import net.runelite.api.Player; -import net.runelite.api.gameval.NpcID; -import net.runelite.api.gameval.SpotanimID; -import net.runelite.client.game.NPCManager; - -public class Attacking -{ - // These animations are the ones which exceed the duration of their attack cooldown - // so in this case DO NOT fall back the animation as it is un-reliable. - private static final Set UNRELIABLE_ANIMATIONS = new ImmutableSet.Builder() - .add(AnimationData.RANGED_BLOWPIPE).add(AnimationData.RANGED_BLAZING_BLOWPIPE) - .add(AnimationData.MAGIC_EYE_OF_AYAK).add(AnimationData.MAGIC_EYE_OF_AYAK_SPEC).build(); - - // Combat Dummy + Nightmare Pillars - private static final Set SPECIAL_NPCS = new ImmutableSet.Builder() - .add(NpcID.POH_COMBAT_DUMMY_UPGRADED_UNDEADSLAYER_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_ETHER_NPC, - NpcID.POH_COMBAT_DUMMY_UPGRADED_KQ_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_KURASK_NPC, - NpcID.POH_COMBAT_DUMMY_UPGRADED_VAMPIRE_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_VORKATH_NPC, - NpcID.POH_COMBAT_DUMMY_NPC, NpcID.POH_COMBAT_DUMMY_UNDEADSLAYER_NPC, - NpcID.POH_COMBAT_DUMMY_UPGRADED_NPC, NpcID.NIGHTMARE_TOTEM_1_READY, NpcID.NIGHTMARE_TOTEM_2_READY, - NpcID.NIGHTMARE_TOTEM_3_READY, NpcID.NIGHTMARE_TOTEM_4_READY) - .build(); - - public static boolean isPlayerAttacking(final Client client, final NPCManager npcManager) - { - final Player localPlayer = client.getLocalPlayer(); - final int animationId = localPlayer.getAnimation(); - if (AnimationData.isBlockListAnimation(animationId)) - { - return false; - } - - // Not walking is either ANY player animation or the edge cases which don't trigger an animation, - // e.g Salamander. - final boolean notWalking = animationId != -1 || getSalamanderAttack(client); - - // Testing if we are attacking by checking the target is more future proof to new weapons which - // don't - // need custom code and the weapon stats are enough. - final Actor target = localPlayer.getInteracting(); - if (target != null && (target instanceof NPC)) - { - final NPC npc = (NPC) target; - final boolean containsAttackOption = Arrays.stream(npc.getComposition().getActions()) - .anyMatch("Attack"::equals); - final Integer health = npcManager.getHealth(npc.getId()); - final boolean hasHealthAndLevel = health != null && health > 0 && target.getCombatLevel() > 0; - final boolean attackingNPC = hasHealthAndLevel || SPECIAL_NPCS.contains(npc.getId()) - || containsAttackOption; - // just having a target is not enough the player may be out of range, we must wait for any - // animation which isn't running/walking/etc - return attackingNPC && notWalking; - } - if (target != null && (target instanceof Player)) - { - return notWalking; - } - if (target == null) - { - // Not attacking anything - return false; - } - - // Do not use any animations from this set - final AnimationData fromId = AnimationData.fromId(animationId); - if (UNRELIABLE_ANIMATIONS.contains(fromId)) - { - return false; - } - // fall back to animations. - return fromId != null; - } - - public static Attack PlayerAttack(final Client client) - { - final Player localPlayer = client.getLocalPlayer(); - final int animationId = localPlayer.getAnimation(); - final Actor target = localPlayer.getInteracting(); - return new Attack(animationId, getSalamanderAttack(client), target); - } - - private static boolean getSalamanderAttack(final Client client) - { - return client.getLocalPlayer().hasSpotAnim(SpotanimID.FIREBREATH); - } -} +package com.attacktimer.Attacking; + +/* + * Copyright (c) 2022, Nick Graves + * Copyright (c) 2024-2026, Lexer747 + * Copyright (c) 2024-2026, Richardant + * 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 com.attacktimer.AnimationData; +import com.google.common.collect.ImmutableSet; +import java.util.Arrays; +import java.util.Set; +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.gameval.NpcID; +import net.runelite.api.gameval.SpotanimID; +import net.runelite.client.game.NPCManager; + +public class Attacking +{ + // These animations are the ones which exceed the duration of their attack cooldown + // so in this case DO NOT fall back the animation as it is un-reliable. + private static final Set UNRELIABLE_ANIMATIONS = new ImmutableSet.Builder() + .add(AnimationData.RANGED_BLOWPIPE).add(AnimationData.RANGED_BLAZING_BLOWPIPE) + .add(AnimationData.MAGIC_EYE_OF_AYAK).add(AnimationData.MAGIC_EYE_OF_AYAK_SPEC).build(); + + // Combat Dummy + Nightmare Pillars + private static final Set SPECIAL_NPCS = new ImmutableSet.Builder() + .add(NpcID.POH_COMBAT_DUMMY_UPGRADED_UNDEADSLAYER_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_ETHER_NPC, + NpcID.POH_COMBAT_DUMMY_UPGRADED_KQ_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_KURASK_NPC, + NpcID.POH_COMBAT_DUMMY_UPGRADED_VAMPIRE_NPC, NpcID.POH_COMBAT_DUMMY_UPGRADED_VORKATH_NPC, + NpcID.POH_COMBAT_DUMMY_NPC, NpcID.POH_COMBAT_DUMMY_UNDEADSLAYER_NPC, + NpcID.POH_COMBAT_DUMMY_UPGRADED_NPC, NpcID.NIGHTMARE_TOTEM_1_READY, NpcID.NIGHTMARE_TOTEM_2_READY, + NpcID.NIGHTMARE_TOTEM_3_READY, NpcID.NIGHTMARE_TOTEM_4_READY) + .build(); + + public static boolean isPlayerAttacking(final Client client, final NPCManager npcManager) + { + final Player localPlayer = client.getLocalPlayer(); + final int animationId = localPlayer.getAnimation(); + if (AnimationData.isBlockListAnimation(animationId)) + { + return false; + } + + // Not walking is either ANY player animation or the edge cases which don't trigger an animation, + // e.g Salamander. + final boolean notWalking = animationId != -1 || getSalamanderAttack(client); + + // Testing if we are attacking by checking the target is more future proof to new weapons which + // don't + // need custom code and the weapon stats are enough. + final Actor target = localPlayer.getInteracting(); + if (target != null && (target instanceof NPC)) + { + final NPC npc = (NPC) target; + final boolean containsAttackOption = Arrays.stream(npc.getComposition().getActions()) + .anyMatch("Attack"::equals); + final Integer health = npcManager.getHealth(npc.getId()); + final boolean hasHealthAndLevel = health != null && health > 0 && target.getCombatLevel() > 0; + final boolean attackingNPC = hasHealthAndLevel || SPECIAL_NPCS.contains(npc.getId()) + || containsAttackOption; + // just having a target is not enough the player may be out of range, we must wait for any + // animation which isn't running/walking/etc + return attackingNPC && notWalking; + } + if (target != null && (target instanceof Player)) + { + return notWalking; + } + if (target == null) + { + // Not attacking anything + return false; + } + + // Do not use any animations from this set + final AnimationData fromId = AnimationData.fromId(animationId); + if (UNRELIABLE_ANIMATIONS.contains(fromId)) + { + return false; + } + // fall back to animations. + return fromId != null; + } + + public static Attack PlayerAttack(final Client client) + { + final Player localPlayer = client.getLocalPlayer(); + final int animationId = localPlayer.getAnimation(); + final Actor target = localPlayer.getInteracting(); + return new Attack(animationId, getSalamanderAttack(client), target); + } + + private static boolean getSalamanderAttack(final Client client) + { + return client.getLocalPlayer().hasSpotAnim(SpotanimID.FIREBREATH); + } +} diff --git a/src/main/java/com/attacktimer/ManualCasting.java b/src/main/java/com/attacktimer/ManualCasting.java index b4e4e49..e63fc1f 100644 --- a/src/main/java/com/attacktimer/ManualCasting.java +++ b/src/main/java/com/attacktimer/ManualCasting.java @@ -1,49 +1,49 @@ -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 net.runelite.api.Client; - -public class ManualCasting -{ - /** - * is returns true if the plugin believes the player is currently manually casting a spell - */ - public static boolean is(final Client client, final AnimationData curId, final int soundEffectTick, final int soundEffectId) - { - // If you use a weapon like a blow pipe which has an animation longer than it's cool down then cast an - // ancient attack it wont have an animation at all. We can therefore need to detect this with a list - // of sounds instead. This obviously doesn't work if the player is muted. ATM I can't think of a way - // to detect this type of attack as a cast, only sound is an indication that the player is on - // cooldown, melee attacks, etc will trigger an animation overwriting the last frame of the blowpipe's - // idle animation. - final boolean castingFromSound = client.getTickCount() == soundEffectTick - ? CastingSoundData.isCastingSound(soundEffectId) - : false; - final boolean castingFromAnimation = AnimationData.isManualCasting(curId); - return castingFromSound || castingFromAnimation; - } -} +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 net.runelite.api.Client; + +public class ManualCasting +{ + /** + * is returns true if the plugin believes the player is currently manually casting a spell + */ + public static boolean is(final Client client, final AnimationData curId, final int soundEffectTick, final int soundEffectId) + { + // If you use a weapon like a blow pipe which has an animation longer than it's cool down then cast an + // ancient attack it wont have an animation at all. We can therefore need to detect this with a list + // of sounds instead. This obviously doesn't work if the player is muted. ATM I can't think of a way + // to detect this type of attack as a cast, only sound is an indication that the player is on + // cooldown, melee attacks, etc will trigger an animation overwriting the last frame of the blowpipe's + // idle animation. + final boolean castingFromSound = client.getTickCount() == soundEffectTick + ? CastingSoundData.isCastingSound(soundEffectId) + : false; + final boolean castingFromAnimation = AnimationData.isManualCasting(curId); + return castingFromSound || castingFromAnimation; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java index 17a4901..3dfeff0 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java +++ b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java @@ -1,191 +1,191 @@ -package com.attacktimer.VariableSpeed; - -/* - * 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 com.attacktimer.AnimationData; -import com.attacktimer.AttackProcedure; -import com.attacktimer.AttackSpeed; -import com.attacktimer.Attacking.Attacking; -import com.attacktimer.ClientUtils.Utils; -import com.attacktimer.Spellbook; -import com.attacktimer.VariableSpeed.State.TickCount; -import com.google.common.collect.ImmutableSet; -import java.util.Set; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.NPC; -import net.runelite.api.gameval.AnimationID; -import net.runelite.api.gameval.ItemID; -import net.runelite.api.gameval.NpcID; -import net.runelite.client.game.ItemManager; - -@Slf4j -public class DoomOfMokhaiotl implements IVariableSpeed -{ - // As you delve deeper you change region - private static final Set DOOM_REGION_IDS = new ImmutableSet.Builder() - .add(5269) // Delve 1 - .add(13668) // Delve 2 - 5 - .add(14180) // Delve 5+ - .build(); - // All larvae and the volatile earth work the same: - // - if you attack them with demon bane you get no CD. - // - else non demonbane overwrites you're current CD - // - you can attack them whilst on CD. - private static final Set COOLDOWN_OVERWRITES_IDS = new ImmutableSet.Builder() - .add(NpcID.DOM_DEMONIC_ENERGY) - .add(NpcID.DOM_DEMONIC_ENERGY_GIANT_MAGE) - .add(NpcID.DOM_DEMONIC_ENERGY_GIANT_RANGE) - .add(NpcID.DOM_DEMONIC_ENERGY_MAGE) - .add(NpcID.DOM_DEMONIC_ENERGY_RANGE) - .add(NpcID.DOM_DEMONIC_ENERGY_MELEE) - .add(NpcID.DOM_SHOCKWAVE_PATH_NODE) - .build(); - - private static final Set NO_COOLDOWN_WEAPON = new ImmutableSet.Builder() - .add(ItemID.SILVERLIGHT) - .add(ItemID.DARKLIGHT) - .add(ItemID.ARCLIGHT) - .add(ItemID.EMBERLIGHT) - .add(ItemID.BONE_CLAWS) - .add(ItemID.SCORCHING_BOW) - .add(ItemID.HOLY_WATER) - .add(ItemID.EYE_OF_AYAK) - .build(); - - private final TickCount tickCount; - private final AttackSpeed attackSpeed; - private int larvaeConsumed = -1; - private int shieldConsumed = -1; - - DoomOfMokhaiotl(final TickCount tc, final AttackSpeed attackSpeed) - { - this.tickCount = tc; - this.attackSpeed = attackSpeed; - } - - // https://oldschool.runescape.wiki/w/Doom_of_Mokhaiotl/Strategies#Demonic_larvae - // - // They may be attacked on attack cooldown: Non-demonbane attacks incur the weapon's attack delay - // afterwards, whereas demonbane attacks and the Eye of Ayak will not incur any attack delay. - // - // Doom can also be attacked whilst on cooldown when he is charging his shield (melee punish only) - // - // The https://oldschool.runescape.wiki/w/Volatile_earth can also be attacked with no attack delay. - public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, final Spellbook spellbook, final boolean debugLogs) - { - if (!Utils.isInRegionId(client, DOOM_REGION_IDS)) - { - return attackDelayHoldoffTicks; - } - - final var atk = Attacking.PlayerAttack(client); - final AnimationData anim = AnimationData.fromId(atk.getAnimationId()); - if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC)) - { - return attackDelayHoldoffTicks; - } - - final NPC npc = (NPC) atk.getTarget(); - final int npcId = npc.getId(); - if (COOLDOWN_OVERWRITES_IDS.contains(npcId)) - { - if (tickCount.isWithinNTicks(larvaeConsumed, 1)) - { - return attackDelayHoldoffTicks; - } - final int weaponId = Utils.getWeaponId(client); - final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(anim) && anim == AnimationData.MAGIC_ARCEUUS_DEMONBANE; - if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell) - { - return attackDelayHoldoffTicks; - } - - if (debugLogs) - { - log.debug("DoomOfMokhaiotl success, attacking larvae with normal weapon"); - } - larvaeConsumed = tickCount.get(); - return attackSpeed.compute(client, anim, spellbook, itemManager); - } - else if (npcId == NpcID.DOM_BOSS) - { - if (tickCount.isWithinNTicks(shieldConsumed, 30)) - { - return attackDelayHoldoffTicks; - } - final var animId = npc.getAnimation(); - // Undocumented in the wiki but from my testing these can be hit while on cooldown but unlike the - // grubs do add up the delay. This is original research: - // - // Atk (5) 1923 -> Chally (7) 1926 -> If plain off CD 1935 (actual: 1933 = 2 tick reduction) (3 tick gap between punish) - // Atk (5) 1945 -> Swift blade (3) 1946 -> If plain off CD 1953 (actual: 1949 = 4 tick reduction) (1 tick gap between punish) - // Atk (5) 1964 -> Rapier (4) 1966 -> If plain off CD 1973 (actual: 1970 = 3 tick reduction) (2 tick gap between punish) - // Atk (5) 2046 -> Battle axe (6) 2048 -> If plain off CD 2057 (actual: 2054 = 3 tick reduction) (2 tick gap between punish) - // Atk (5) 4317 -> Rapier (4) 4318 -> If plain off CD 4326 (actual: 4322 = 4 tick reduction) (1 tick gap between punish) - // - // Therefore my conclusion based off these samples is that resulting delay is just the attack - // delay of the weapon used, overwriting the current delay not addition. - if (animId == AnimationID.DOM_BEAM_CHARGE_LOOP || animId == AnimationID.DOM_BEAM_CHARGE) - { - if (Utils.getAttackType(client).IsMelee()) - { - if (debugLogs) - { - log.debug("DoomOfMokhaiotl success, on cooldown melee swing"); - } - shieldConsumed = tickCount.get(); - return attackSpeed.compute(client, anim, spellbook, itemManager); - } - } - return attackDelayHoldoffTicks; - } - else - { - return attackDelayHoldoffTicks; - } - } - - // Take care here to ensure no infinite loop or affect on the speed as the onRender does call this via variable speed - public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, - final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed, - final int curSpeed) - { - final int targetId = Utils.getTargetId(client); - final boolean inDoom = Utils.isInRegionId(client, DOOM_REGION_IDS); - if (inDoom && COOLDOWN_OVERWRITES_IDS.contains(targetId)) - { - final int weaponId = Utils.getWeaponId(client); - final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(curAnimation) && curAnimation == AnimationData.MAGIC_ARCEUUS_DEMONBANE; - if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell) - { - log.debug("DoomOfMokhaiotl success, zero delay grub"); - return 1; - } - } - return curSpeed; - } -} +package com.attacktimer.VariableSpeed; + +/* + * 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 com.attacktimer.AnimationData; +import com.attacktimer.AttackProcedure; +import com.attacktimer.AttackSpeed; +import com.attacktimer.Attacking.Attacking; +import com.attacktimer.ClientUtils.Utils; +import com.attacktimer.Spellbook; +import com.attacktimer.VariableSpeed.State.TickCount; +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.gameval.AnimationID; +import net.runelite.api.gameval.ItemID; +import net.runelite.api.gameval.NpcID; +import net.runelite.client.game.ItemManager; + +@Slf4j +public class DoomOfMokhaiotl implements IVariableSpeed +{ + // As you delve deeper you change region + private static final Set DOOM_REGION_IDS = new ImmutableSet.Builder() + .add(5269) // Delve 1 + .add(13668) // Delve 2 - 5 + .add(14180) // Delve 5+ + .build(); + // All larvae and the volatile earth work the same: + // - if you attack them with demon bane you get no CD. + // - else non demonbane overwrites you're current CD + // - you can attack them whilst on CD. + private static final Set COOLDOWN_OVERWRITES_IDS = new ImmutableSet.Builder() + .add(NpcID.DOM_DEMONIC_ENERGY) + .add(NpcID.DOM_DEMONIC_ENERGY_GIANT_MAGE) + .add(NpcID.DOM_DEMONIC_ENERGY_GIANT_RANGE) + .add(NpcID.DOM_DEMONIC_ENERGY_MAGE) + .add(NpcID.DOM_DEMONIC_ENERGY_RANGE) + .add(NpcID.DOM_DEMONIC_ENERGY_MELEE) + .add(NpcID.DOM_SHOCKWAVE_PATH_NODE) + .build(); + + private static final Set NO_COOLDOWN_WEAPON = new ImmutableSet.Builder() + .add(ItemID.SILVERLIGHT) + .add(ItemID.DARKLIGHT) + .add(ItemID.ARCLIGHT) + .add(ItemID.EMBERLIGHT) + .add(ItemID.BONE_CLAWS) + .add(ItemID.SCORCHING_BOW) + .add(ItemID.HOLY_WATER) + .add(ItemID.EYE_OF_AYAK) + .build(); + + private final TickCount tickCount; + private final AttackSpeed attackSpeed; + private int larvaeConsumed = -1; + private int shieldConsumed = -1; + + DoomOfMokhaiotl(final TickCount tc, final AttackSpeed attackSpeed) + { + this.tickCount = tc; + this.attackSpeed = attackSpeed; + } + + // https://oldschool.runescape.wiki/w/Doom_of_Mokhaiotl/Strategies#Demonic_larvae + // + // They may be attacked on attack cooldown: Non-demonbane attacks incur the weapon's attack delay + // afterwards, whereas demonbane attacks and the Eye of Ayak will not incur any attack delay. + // + // Doom can also be attacked whilst on cooldown when he is charging his shield (melee punish only) + // + // The https://oldschool.runescape.wiki/w/Volatile_earth can also be attacked with no attack delay. + public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, final Spellbook spellbook, final boolean debugLogs) + { + if (!Utils.isInRegionId(client, DOOM_REGION_IDS)) + { + return attackDelayHoldoffTicks; + } + + final var atk = Attacking.PlayerAttack(client); + final AnimationData anim = AnimationData.fromId(atk.getAnimationId()); + if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC)) + { + return attackDelayHoldoffTicks; + } + + final NPC npc = (NPC) atk.getTarget(); + final int npcId = npc.getId(); + if (COOLDOWN_OVERWRITES_IDS.contains(npcId)) + { + if (tickCount.isWithinNTicks(larvaeConsumed, 1)) + { + return attackDelayHoldoffTicks; + } + final int weaponId = Utils.getWeaponId(client); + final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(anim) && anim == AnimationData.MAGIC_ARCEUUS_DEMONBANE; + if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell) + { + return attackDelayHoldoffTicks; + } + + if (debugLogs) + { + log.debug("DoomOfMokhaiotl success, attacking larvae with normal weapon"); + } + larvaeConsumed = tickCount.get(); + return attackSpeed.compute(client, anim, spellbook, itemManager); + } + else if (npcId == NpcID.DOM_BOSS) + { + if (tickCount.isWithinNTicks(shieldConsumed, 30)) + { + return attackDelayHoldoffTicks; + } + final var animId = npc.getAnimation(); + // Undocumented in the wiki but from my testing these can be hit while on cooldown but unlike the + // grubs do add up the delay. This is original research: + // + // Atk (5) 1923 -> Chally (7) 1926 -> If plain off CD 1935 (actual: 1933 = 2 tick reduction) (3 tick gap between punish) + // Atk (5) 1945 -> Swift blade (3) 1946 -> If plain off CD 1953 (actual: 1949 = 4 tick reduction) (1 tick gap between punish) + // Atk (5) 1964 -> Rapier (4) 1966 -> If plain off CD 1973 (actual: 1970 = 3 tick reduction) (2 tick gap between punish) + // Atk (5) 2046 -> Battle axe (6) 2048 -> If plain off CD 2057 (actual: 2054 = 3 tick reduction) (2 tick gap between punish) + // Atk (5) 4317 -> Rapier (4) 4318 -> If plain off CD 4326 (actual: 4322 = 4 tick reduction) (1 tick gap between punish) + // + // Therefore my conclusion based off these samples is that resulting delay is just the attack + // delay of the weapon used, overwriting the current delay not addition. + if (animId == AnimationID.DOM_BEAM_CHARGE_LOOP || animId == AnimationID.DOM_BEAM_CHARGE) + { + if (Utils.getAttackType(client).IsMelee()) + { + if (debugLogs) + { + log.debug("DoomOfMokhaiotl success, on cooldown melee swing"); + } + shieldConsumed = tickCount.get(); + return attackSpeed.compute(client, anim, spellbook, itemManager); + } + } + return attackDelayHoldoffTicks; + } + else + { + return attackDelayHoldoffTicks; + } + } + + // Take care here to ensure no infinite loop or affect on the speed as the onRender does call this via variable speed + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed, + final int curSpeed) + { + final int targetId = Utils.getTargetId(client); + final boolean inDoom = Utils.isInRegionId(client, DOOM_REGION_IDS); + if (inDoom && COOLDOWN_OVERWRITES_IDS.contains(targetId)) + { + final int weaponId = Utils.getWeaponId(client); + final boolean isDemonbaneSpell = spellbook == Spellbook.ARCEUUS && AnimationData.isManualCasting(curAnimation) && curAnimation == AnimationData.MAGIC_ARCEUUS_DEMONBANE; + if (NO_COOLDOWN_WEAPON.contains(weaponId) || isDemonbaneSpell) + { + log.debug("DoomOfMokhaiotl success, zero delay grub"); + return 1; + } + } + return curSpeed; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java index 61b8f7f..a418b07 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java +++ b/src/main/java/com/attacktimer/VariableSpeed/MaggotKing.java @@ -1,91 +1,91 @@ -package com.attacktimer.VariableSpeed; - -/* - * 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 com.attacktimer.AnimationData; -import com.attacktimer.AttackSpeed; -import com.attacktimer.Attacking.Attacking; -import com.attacktimer.ClientUtils.Utils; -import com.attacktimer.Spellbook; -import com.attacktimer.VariableSpeed.State.TickCount; -import lombok.extern.slf4j.Slf4j; -import net.runelite.api.Client; -import net.runelite.api.NPC; -import net.runelite.api.gameval.AnimationID; -import net.runelite.api.gameval.NpcID; -import net.runelite.client.game.ItemManager; - -@Slf4j -public class MaggotKing -{ - private static final int MAGGOT_KING_REGION_ID = 11645; - - private final TickCount tickCount; - private final AttackSpeed attackSpeed; - // the tick count if a larvae was hit, this is purely to debounce - private int consumed = -1; - - MaggotKing(final TickCount tc, final AttackSpeed attackSpeed) - { - this.tickCount = tc; - this.attackSpeed = attackSpeed; - } - - // https://oldschool.runescape.wiki/w/Maggot_King/Strategies#Ur-maggot_larvae - // - // If the player attacks a maggot with a "standard bow" it doesn't matter what current cooldown is the - // player is immediately set to the cooldown of the bow they used. - // - // Therefore this method returns `attackDelayHoldoffTicks` in all cases where this condition isn't met. - // But if the condition is met this method returns a brand new number which is the attack speed of the bow - // used. This number can be the same as the current delay and that's ok. - public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, final Spellbook spellbook, final boolean debugLogs) - { - if (!Utils.isInRegionId(client, MAGGOT_KING_REGION_ID) || tickCount.isWithinNTicks(consumed, 1)) - { - return attackDelayHoldoffTicks; - } - - final var atk = Attacking.PlayerAttack(client); - final AnimationData anim = AnimationData.fromId(atk.getAnimationId()); - if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC)) - { - return attackDelayHoldoffTicks; - } - - final NPC npc = (NPC) atk.getTarget(); - if (npc.getId() != NpcID.UR_MAGGOT_LARVAE || !anim.isStandardBowAttack() || npc.getAnimation() != AnimationID.UR_MAGGOT_LARVAE_FLY) - { - return attackDelayHoldoffTicks; - } - if (debugLogs) - { - log.debug("MaggotKing success, attacking flying maggot with bow"); - } - consumed = tickCount.get(); - return attackSpeed.compute(client, anim, spellbook, itemManager); - } -} +package com.attacktimer.VariableSpeed; + +/* + * 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 com.attacktimer.AnimationData; +import com.attacktimer.AttackSpeed; +import com.attacktimer.Attacking.Attacking; +import com.attacktimer.ClientUtils.Utils; +import com.attacktimer.Spellbook; +import com.attacktimer.VariableSpeed.State.TickCount; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.gameval.AnimationID; +import net.runelite.api.gameval.NpcID; +import net.runelite.client.game.ItemManager; + +@Slf4j +public class MaggotKing +{ + private static final int MAGGOT_KING_REGION_ID = 11645; + + private final TickCount tickCount; + private final AttackSpeed attackSpeed; + // the tick count if a larvae was hit, this is purely to debounce + private int consumed = -1; + + MaggotKing(final TickCount tc, final AttackSpeed attackSpeed) + { + this.tickCount = tc; + this.attackSpeed = attackSpeed; + } + + // https://oldschool.runescape.wiki/w/Maggot_King/Strategies#Ur-maggot_larvae + // + // If the player attacks a maggot with a "standard bow" it doesn't matter what current cooldown is the + // player is immediately set to the cooldown of the bow they used. + // + // Therefore this method returns `attackDelayHoldoffTicks` in all cases where this condition isn't met. + // But if the condition is met this method returns a brand new number which is the attack speed of the bow + // used. This number can be the same as the current delay and that's ok. + public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, final Spellbook spellbook, final boolean debugLogs) + { + if (!Utils.isInRegionId(client, MAGGOT_KING_REGION_ID) || tickCount.isWithinNTicks(consumed, 1)) + { + return attackDelayHoldoffTicks; + } + + final var atk = Attacking.PlayerAttack(client); + final AnimationData anim = AnimationData.fromId(atk.getAnimationId()); + if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC)) + { + return attackDelayHoldoffTicks; + } + + final NPC npc = (NPC) atk.getTarget(); + if (npc.getId() != NpcID.UR_MAGGOT_LARVAE || !anim.isStandardBowAttack() || npc.getAnimation() != AnimationID.UR_MAGGOT_LARVAE_FLY) + { + return attackDelayHoldoffTicks; + } + if (debugLogs) + { + log.debug("MaggotKing success, attacking flying maggot with bow"); + } + consumed = tickCount.get(); + return attackSpeed.compute(client, anim, spellbook, itemManager); + } +} From 33643be4109d9bb1d04e9e01c0a1df804c16e04d Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sun, 30 Aug 2026 14:09:47 +0100 Subject: [PATCH 14/16] deprecate properly --- src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java b/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java index f54a22b..24a5969 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomeConfig.java @@ -41,7 +41,6 @@ public interface AttackTimerMetronomeConfig extends Config { /** * @deprecated - * @return */ @ConfigItem( position = 0, @@ -50,6 +49,7 @@ public interface AttackTimerMetronomeConfig extends Config description = "Enable visual metronome", hidden = true // deprecated ) + @Deprecated default boolean enableMetronome() { return true; From 90d3e763d3304175907d487aa44d83ad3499bb9a Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sun, 30 Aug 2026 14:32:34 +0100 Subject: [PATCH 15/16] check blocklist for doom --- .../java/com/attacktimer/AnimationData.java | 17 +++++++++++------ .../VariableSpeed/DoomOfMokhaiotl.java | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/attacktimer/AnimationData.java b/src/main/java/com/attacktimer/AnimationData.java index 82bff50..8e80bfa 100644 --- a/src/main/java/com/attacktimer/AnimationData.java +++ b/src/main/java/com/attacktimer/AnimationData.java @@ -414,6 +414,17 @@ public boolean matchesSpellbook(Spellbook s) return false; } + // isStandardBowAttack returns true if the animation is performed by a bow https://oldschool.runescape.wiki/w/Standard_ranged_weapons + public boolean isStandardBowAttack() + { + return STANDARD_BOW_ATTACKS.contains(this.animationId); + } + + public boolean isBlockListAnimation() + { + return NOT_ATTACKS.containsKey(this.animationId); + } + // An enum of combat styles (including stab, slash, crush). public enum AttackStyle @@ -459,10 +470,4 @@ public String toString() return StringUtils.capitalize(super.toString().toLowerCase()); } } - - // isStandardBowAttack returns true if the animation is performed by a bow https://oldschool.runescape.wiki/w/Standard_ranged_weapons - public boolean isStandardBowAttack() - { - return STANDARD_BOW_ATTACKS.contains(this.animationId); - } } diff --git a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java index 3dfeff0..dddf4cc 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java +++ b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java @@ -104,7 +104,7 @@ public int onRender(final Client client, final ItemManager itemManager, final in final var atk = Attacking.PlayerAttack(client); final AnimationData anim = AnimationData.fromId(atk.getAnimationId()); - if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC)) + if (anim == null || atk.getTarget() == null || !(atk.getTarget() instanceof NPC) || anim.isBlockListAnimation()) { return attackDelayHoldoffTicks; } From 8be218ef3108558307081d1c56400fffc03680f7 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sun, 30 Aug 2026 15:01:57 +0100 Subject: [PATCH 16/16] clean up --- .../java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java | 4 +++- .../java/com/attacktimer/VariableSpeed/VariableSpeed.java | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java index dddf4cc..1c15e33 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java +++ b/src/main/java/com/attacktimer/VariableSpeed/DoomOfMokhaiotl.java @@ -65,6 +65,7 @@ public class DoomOfMokhaiotl implements IVariableSpeed .add(NpcID.DOM_SHOCKWAVE_PATH_NODE) .build(); + // There's no actual demonbane stat anywhere in the runelite API so this list has to be hardcoded private static final Set NO_COOLDOWN_WEAPON = new ImmutableSet.Builder() .add(ItemID.SILVERLIGHT) .add(ItemID.DARKLIGHT) @@ -94,7 +95,8 @@ public class DoomOfMokhaiotl implements IVariableSpeed // // Doom can also be attacked whilst on cooldown when he is charging his shield (melee punish only) // - // The https://oldschool.runescape.wiki/w/Volatile_earth can also be attacked with no attack delay. + // The https://oldschool.runescape.wiki/w/Volatile_earth also has the same larvae mechanics and allow list + // of items. public int onRender(final Client client, final ItemManager itemManager, final int attackDelayHoldoffTicks, final Spellbook spellbook, final boolean debugLogs) { if (!Utils.isInRegionId(client, DOOM_REGION_IDS)) diff --git a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java index 492228e..8e8496d 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java +++ b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java @@ -105,7 +105,6 @@ public static void onNpcDespawned(final Client client, final NpcDespawned npcDes private static final Yama YAMA = new Yama(); private static final MarkOfDarkness MARK_OF_DARKNESS = new MarkOfDarkness(); - public static final RapidAttackStyle RAPID_ATTACK_STYLE = new RapidAttackStyle(); // Variable speed that doesn't neatly fit in to the IVariable speed pattern (it's not weapon related // but boss related). @@ -127,7 +126,7 @@ public static void onNpcDespawned(final Client client, final NpcDespawned npcDes // Incremental: new BloodMoonSet(), - RAPID_ATTACK_STYLE, + new RapidAttackStyle(), new RedKerisSpec(), new PurgingStaffSpec(YAMA), new EyeOfAyak(),