From da08885df62aeb7471f16b1d73c9fef58f0417b4 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Thu, 16 Apr 2026 00:41:00 +0100 Subject: [PATCH 1/2] Add Yama, purging staff and fireballs --- .github/pull_request_template.md | 4 +- build.gradle | 1 - .../java/com/attacktimer/AnimationData.java | 2 +- .../java/com/attacktimer/AttackStyle.java | 6 + .../AttackTimerMetronomePlugin.java | 139 +++++++- .../AttackTimerMetronomeTileOverlay.java | 1 + .../com/attacktimer/ClientUtils/Utils.java | 63 +++- .../java/com/attacktimer/PoweredStaves.java | 2 +- src/main/java/com/attacktimer/Spellbook.java | 2 +- .../VariableSpeed/BloodMoonSet.java | 8 +- .../attacktimer/VariableSpeed/EyeOfAyak.java | 5 +- .../VariableSpeed/IVariableSpeed.java | 49 +-- .../VariableSpeed/Leagues4and5.java | 90 ----- .../VariableSpeed/PurgingStaffSpec.java | 89 +++++ .../VariableSpeed/RapidAttackStyle.java | 9 +- .../VariableSpeed/RedKerisSpec.java | 7 +- .../attacktimer/VariableSpeed/Scurrius.java | 5 +- .../VariableSpeed/ShadowCrash.java | 312 ++++++++++++++++++ .../VariableSpeed/State/IStateTracker.java | 53 +++ .../VariableSpeed/State/MarkOfDarkness.java | 89 +++++ .../VariableSpeed/State/TickCount.java | 60 ++++ .../attacktimer/VariableSpeed/State/Yama.java | 202 ++++++++++++ .../VariableSpeed/State/YamaPhase.java | 45 +++ .../VariableSpeed/TombsOfAmascut.java | 5 +- .../VariableSpeed/TormentedDemons.java | 31 +- .../VariableSpeed/VariableSpeed.java | 57 +++- .../attacktimer/VariableSpeed/shadowCrash.png | Bin 0 -> 22515 bytes 27 files changed, 1146 insertions(+), 190 deletions(-) delete mode 100644 src/main/java/com/attacktimer/VariableSpeed/Leagues4and5.java create mode 100644 src/main/java/com/attacktimer/VariableSpeed/PurgingStaffSpec.java create mode 100644 src/main/java/com/attacktimer/VariableSpeed/ShadowCrash.java create mode 100644 src/main/java/com/attacktimer/VariableSpeed/State/IStateTracker.java create mode 100644 src/main/java/com/attacktimer/VariableSpeed/State/MarkOfDarkness.java create mode 100644 src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java create mode 100644 src/main/java/com/attacktimer/VariableSpeed/State/Yama.java create mode 100644 src/main/java/com/attacktimer/VariableSpeed/State/YamaPhase.java create mode 100644 src/main/java/com/attacktimer/VariableSpeed/shadowCrash.png diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1cc7d1f..9bbe422 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ Please explain in reasonable detail what your series of changes does, and how it does it. Make sure to link any relevant github issues which instigated your changes. -you can safely leave delete this comment as you fill out this text box. +you can safely leave or delete this comment as you fill out this text box. Please leave the Summary and Testing sub titles. --> @@ -14,6 +14,6 @@ Please leave the Summary and Testing sub titles. DO NOT leave this section empty, please add any relevant testing evidence (in game, unit tests, etc) that show that your changes achieve the desired effect. -you can safely leave delete this comment as you fill out this text box. +you can safely leave or delete this comment as you fill out this text box. Please leave the Summary and Testing sub titles. --> \ No newline at end of file diff --git a/build.gradle b/build.gradle index fbad105..51135c9 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,6 @@ java { } group = 'com.attacktimer' -version = '1.1' tasks.withType(JavaCompile) { options.encoding = 'UTF-8' diff --git a/src/main/java/com/attacktimer/AnimationData.java b/src/main/java/com/attacktimer/AnimationData.java index 3fad116..c1788e9 100644 --- a/src/main/java/com/attacktimer/AnimationData.java +++ b/src/main/java/com/attacktimer/AnimationData.java @@ -187,7 +187,7 @@ public enum AnimationData MAGIC_ANCIENT_MULTI_TARGET_PVP(1979, AttackStyle.MAGIC, Spellbook.ANCIENT), // Burst & Barrage animations (tested all 8, different weapons) MAGIC_ANCIENT_SINGLE_TARGET(10091, AttackStyle.MAGIC, Spellbook.ANCIENT), // Rush & Blitz animations (tested all 8, different weapons) MAGIC_ANCIENT_SINGLE_TARGET_PVP(1978, AttackStyle.MAGIC, Spellbook.ANCIENT), // Rush & Blitz animations - + MAGIC_ARCEUUS_DEMONBANE(8977, 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(8972, AttackStyle.MAGIC, Spellbook.ARCEUUS), diff --git a/src/main/java/com/attacktimer/AttackStyle.java b/src/main/java/com/attacktimer/AttackStyle.java index 1da0c19..e6b2b9f 100644 --- a/src/main/java/com/attacktimer/AttackStyle.java +++ b/src/main/java/com/attacktimer/AttackStyle.java @@ -51,4 +51,10 @@ public enum AttackStyle this.name = name; this.skills = skills; } + + @Override + public String toString() + { + return this.name; + } } \ No newline at end of file diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java index 8428378..133846f 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomePlugin.java @@ -28,7 +28,9 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import com.attacktimer.ClientUtils.Utils; 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; @@ -36,6 +38,7 @@ import java.awt.Color; 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; @@ -44,19 +47,19 @@ import javax.inject.Inject; import net.runelite.api.Actor; import net.runelite.api.Client; -import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.InventoryID; -import net.runelite.api.Item; -import net.runelite.api.ItemContainer; import net.runelite.api.NPC; import net.runelite.api.Player; +import net.runelite.api.Skill; import net.runelite.api.Varbits; import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.FakeXpDrop; import net.runelite.api.events.GameTick; import net.runelite.api.events.InteractingChanged; import net.runelite.api.events.SoundEffectPlayed; +import net.runelite.api.events.StatChanged; import net.runelite.api.events.VarClientIntChanged; import net.runelite.api.events.VarbitChanged; +import net.runelite.api.gameval.VarPlayerID; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; import net.runelite.client.events.ConfigChanged; @@ -123,9 +126,18 @@ public enum AttackState private int lastEquippingMonotonicValue = -1; private int soundEffectTick = -1; private int soundEffectId = -1; + private boolean isUsingMagic = false; public int pendingEatDelayTicks = 0; + private ArrayDeque specialPercentageEvents = new ArrayDeque(); + private Map> combatExpEarned = Map.of( + Skill.MAGIC, new ArrayDeque(), + Skill.RANGED, new ArrayDeque(), + Skill.DEFENCE, new ArrayDeque(), + Skill.STRENGTH, new ArrayDeque(), + Skill.ATTACK, new ArrayDeque() + ); private static final int UI_HIDE_DEBOUNCE_TICKS_MAX = 1; private static final int ATTACK_DELAY_NONE = 0; @@ -188,6 +200,10 @@ public void onVarbitChanged(VarbitChanged varbitChanged) { currentSpellBook = Spellbook.fromVarbit(varbitChanged.getValue()); } + if (varbitChanged.getVarpId() == VarPlayerID.SA_ENERGY) + { + specialPercentageEvents.addLast(varbitChanged.getValue()); + } } // onVarbitChanged happens when the user causes some interaction therefore we can't rely on some fixed @@ -227,6 +243,36 @@ public void onSoundEffectPlayed(SoundEffectPlayed event) soundEffectId = event.getSoundId(); } + @Subscribe + protected void onFakeXpDrop(FakeXpDrop event) + { + if (!combatExpEarned.containsKey(event.getSkill())) + { + return; + } + combatExpEarned.get(event.getSkill()).addLast(event.getXp()); + if (attackState == AttackState.DELAYED_FIRST_TICK) + { + // We recompute attack speed here incase the hitsplat mattered (e.g. purging staff) + performAttack(); + } + } + + @Subscribe + protected void onStatChanged(StatChanged event) + { + if (!combatExpEarned.containsKey(event.getSkill())) + { + return; + } + combatExpEarned.get(event.getSkill()).addLast(event.getXp()); + if (attackState == AttackState.DELAYED_FIRST_TICK) + { + // We recompute attack speed here incase the hitsplat mattered (e.g. purging staff) + performAttack(); + } + } + // endregion @Provides @@ -235,23 +281,41 @@ AttackTimerMetronomeConfig provideConfig(ConfigManager configManager) return configManager.getConfig(AttackTimerMetronomeConfig.class); } - private int getItemIdFromContainer(ItemContainer container, int slotID) + private int computeDamage(AttackStyle attackStyle, AttackProcedure atkType, AnimationData curAnimation) { - if (container == null) + switch (atkType) { - return -1; + case POWERED_STAVE: + // TODO not needed for any variable speed + return -1; + case MANUAL_AUTO_CAST: + if (attackStyle == AttackStyle.DEFENSIVE_CASTING || attackStyle == AttackStyle.DEFENSIVE) + { + // just use the defense exp to compute the damage + return Utils.getLastDelta(combatExpEarned.get(Skill.DEFENCE)); + } + else + { + // deduct the fixed exp based on the spell + // (for now this only works for dark demon bane which awkwardly gives fractional exp) + var mageExp = Utils.getLastDelta(combatExpEarned.get(Skill.MAGIC)); + if (curAnimation != AnimationData.MAGIC_ARCEUUS_DEMONBANE) + { + return -1; + } + return (int) Math.ceil(((double) mageExp - 43.5D) / 2.0D); + } + case MELEE_OR_RANGE: + // TODO not needed for any variable speed + return -1; } - final Item item = container.getItem(slotID); - return (item != null) ? item.getId() : -1; + return -1; } + private int getWeaponId() { - int weaponId = getItemIdFromContainer( - client.getItemContainer(InventoryID.EQUIPMENT), - EquipmentInventorySlot.WEAPON.getSlotIdx() - ); - + final int weaponId = Utils.getWeaponId(client); return WEAPON_ID_MAPPING_WORKAROUNDS.getOrDefault(weaponId, weaponId); } @@ -302,27 +366,35 @@ private int getMagicBaseSpeed(int weaponId) private int getWeaponSpeed(int weaponId, PoweredStaves stave, AnimationData curAnimation, boolean matchesSpellbook) { + var specDelta = Utils.getLastDelta(specialPercentageEvents); + int damageDealt = -1; if (stave != null && stave.getAnimations().contains(curAnimation)) { + isUsingMagic = true; + damageDealt = computeDamage(Utils.getAttackStyle(client), AttackProcedure.POWERED_STAVE, curAnimation); // 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.computeSpeed(client, curAnimation, AttackProcedure.POWERED_STAVE, 4); + return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.POWERED_STAVE, damageDealt, specDelta, 4); } if (matchesSpellbook && isManualCasting(curAnimation)) { + isUsingMagic = true; + damageDealt = computeDamage(Utils.getAttackStyle(client), AttackProcedure.MANUAL_AUTO_CAST, curAnimation); // You can cast with anything equipped in which case we shouldn't look to invent for speed. - return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, getMagicBaseSpeed(weaponId)); + return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MANUAL_AUTO_CAST, damageDealt, specDelta,getMagicBaseSpeed(weaponId)); } + isUsingMagic = false; + damageDealt = computeDamage(Utils.getAttackStyle(client), AttackProcedure.MELEE_OR_RANGE, curAnimation); ItemStats weaponStats = getWeaponStats(weaponId); if (weaponStats == null) { - return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, 4); // Assume barehanded == 4t + return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, damageDealt, specDelta, 4); // Assume barehanded == 4t } // Deadline for next available attack. - return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, weaponStats.getEquipment().getAspeed()); + return VariableSpeed.computeSpeed(client, curAnimation, AttackProcedure.MELEE_OR_RANGE, damageDealt, specDelta, weaponStats.getEquipment().getAspeed()); } private static final List SPECIAL_NPCS = Arrays.asList(10507, 9435, 9438, 9441, 9444); // Combat Dummy + Nightmare Pillars @@ -438,6 +510,7 @@ public void onChatMessage(ChatMessage event) // We should always add eat delay pendingEatDelayTicks += attackDelay; } + VariableSpeed.onChatMessage(client, event); } // onInteractingChanged is the driver for detecting if the player attacked out side the usual tick window @@ -456,6 +529,7 @@ public void onInteractingChanged(InteractingChanged interactingChanged) switch (attackState) { case NOT_ATTACKING: + 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()) @@ -523,6 +597,17 @@ public void onGameTick(GameTick tick) // 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); + if (specialPercentageEvents.size() > 5) + { + specialPercentageEvents.removeFirst(); + } + for (var q : combatExpEarned.values()) + { + if (q.size() > 5) + { + q.removeFirst(); + } + } } @@ -551,6 +636,7 @@ protected void shutDown() throws Exception attackDelayHoldoffTicks = 0; } + @VisibleForTesting public void writeState(ByteArrayDataOutput outChannel) { StringBuilder sb = new StringBuilder(); @@ -570,4 +656,21 @@ public void writeState(ByteArrayDataOutput outChannel) outChannel.write(bytes); } private static final String SEPARATOR = ", "; + + + public void onRender() + { + int delta = 0; + delta = VariableSpeed.SHADOW_CRASH.onRender(client, attackDelayHoldoffTicks, isUsingMagic); + + if (delta != 0) + { + attackDelayHoldoffTicks += delta; + // if a change in attack delay would cause the delay to be less than 0 we hide the display + if (attackDelayHoldoffTicks < 0) + { + attackState = AttackState.NOT_ATTACKING; + } + } + } } diff --git a/src/main/java/com/attacktimer/AttackTimerMetronomeTileOverlay.java b/src/main/java/com/attacktimer/AttackTimerMetronomeTileOverlay.java index 6abbff1..1c559dc 100644 --- a/src/main/java/com/attacktimer/AttackTimerMetronomeTileOverlay.java +++ b/src/main/java/com/attacktimer/AttackTimerMetronomeTileOverlay.java @@ -68,6 +68,7 @@ public AttackTimerMetronomeTileOverlay(Client client, AttackTimerMetronomeConfig @Override public Dimension render(Graphics2D graphics) { + plugin.onRender(); player = client.getLocalPlayer(); plugin.renderedState = plugin.attackState; if (plugin.attackState == AttackTimerMetronomePlugin.AttackState.NOT_ATTACKING) diff --git a/src/main/java/com/attacktimer/ClientUtils/Utils.java b/src/main/java/com/attacktimer/ClientUtils/Utils.java index dbb5bab..939a1ee 100644 --- a/src/main/java/com/attacktimer/ClientUtils/Utils.java +++ b/src/main/java/com/attacktimer/ClientUtils/Utils.java @@ -29,17 +29,20 @@ import com.attacktimer.AttackStyle; import com.attacktimer.AttackType; import com.attacktimer.WeaponType; +import java.util.ArrayDeque; import net.runelite.api.Actor; import net.runelite.api.Client; import net.runelite.api.EquipmentInventorySlot; -import net.runelite.api.InventoryID; import net.runelite.api.Item; import net.runelite.api.ItemContainer; import net.runelite.api.NPC; -import net.runelite.api.VarPlayer; -import net.runelite.api.Varbits; +import net.runelite.api.WorldView; import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.InventoryID; +import net.runelite.api.gameval.VarPlayerID; +import net.runelite.api.gameval.VarbitID; +import org.apache.commons.lang3.ArrayUtils; public class Utils { @@ -55,7 +58,7 @@ public static int getItemIdFromContainer(ItemContainer container, int slotID) public static int getWeaponId(Client client) { - return getItemIdFromContainer(client.getItemContainer(InventoryID.EQUIPMENT), + return getItemIdFromContainer(client.getItemContainer(InventoryID.WORN), EquipmentInventorySlot.WEAPON.getSlotIdx()); } @@ -72,9 +75,17 @@ public static WorldPoint getLocation(Client client) public static AttackStyle getAttackStyle(Client client) { final AttackStyle[] attackStyles = getWeaponType(client).getAttackStyles(client); - final int currentAttackStyleVarbit = client.getVarpValue(VarPlayer.ATTACK_STYLE); + int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE); + final int castingMode = client.getVarbitValue(VarbitID.AUTOCAST_DEFMODE); if (currentAttackStyleVarbit < attackStyles.length) { + // from script4525 + // Even though the client has 5 attack styles for Staffs, only attack styles 0-4 are used, with an additional + // casting mode set for defensive casting + if (currentAttackStyleVarbit == 4) + { + currentAttackStyleVarbit += castingMode; + } return attackStyles[currentAttackStyleVarbit]; } @@ -84,7 +95,7 @@ public static AttackStyle getAttackStyle(Client client) // returns null for unknown weapons public static WeaponType getWeaponType(Client client) { - final int currentEquippedWeaponTypeVarbit = client.getVarbitValue(Varbits.EQUIPPED_WEAPON_TYPE); + final int currentEquippedWeaponTypeVarbit = client.getVarbitValue(VarbitID.COMBAT_WEAPON_CATEGORY); return WeaponType.getWeaponType(currentEquippedWeaponTypeVarbit); } @@ -92,7 +103,7 @@ public static WeaponType getWeaponType(Client client) public static AttackType getAttackType(Client client) { final WeaponType weaponType = getWeaponType(client); - final int currentAttackStyleVarbit = client.getVarpValue(VarPlayer.ATTACK_STYLE); + final int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE); if (currentAttackStyleVarbit < weaponType.getAttackTypes().length) { return weaponType.getAttackTypes()[currentAttackStyleVarbit]; @@ -124,4 +135,42 @@ public static NPC getTargetNPC(Client client) return null; } + // returns true if the client is in the region specified by the id + public static boolean isInRegionId(Client client, int id) + { + final WorldView wv = client.getTopLevelWorldView(); + if (wv == null) + { + return false; + } + + final int[] regions = wv.getMapRegions(); + if (regions == null || regions.length == 0) + { + return false; + } + + return ArrayUtils.contains(regions, 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) + { + int i = 0, last = -1, secondLast = -1; + final var it = events.descendingIterator(); + while (it.hasNext()) + { + if (i == 0) + last = it.next(); + else if (i == 1) + secondLast = it.next(); + else + break; + i++; + } + var delta = last - secondLast; + return delta; + } } diff --git a/src/main/java/com/attacktimer/PoweredStaves.java b/src/main/java/com/attacktimer/PoweredStaves.java index fa9f765..a26aebe 100644 --- a/src/main/java/com/attacktimer/PoweredStaves.java +++ b/src/main/java/com/attacktimer/PoweredStaves.java @@ -40,7 +40,7 @@ // weapons, they cannot be used to auto-cast spells. In addition, they have an attack speed of 4, faster than // other magic weapons, which have an attack speed of 5. The Tumeken's shadow, however, has an attack speed of // 5. This Enum is only to contain the staves which allow magic at 4 ticks. -enum PoweredStaves +public enum PoweredStaves { WEAPON_ACCURSED( Set.of(AnimationData.MAGIC_STANDARD_WAVE_STAFF, AnimationData.MAGIC_ACCURSED_SCEPTRE_SPEC), Projectiles(2337, 2339), 27665, 27666), // https://oldschool.runescape.wiki/w/Accursed_sceptre WEAPON_BLUE_C_STAFF_A(AnimationData.MAGIC_STANDARD_WAVE_STAFF, Projectiles(1720), 23899), // https://oldschool.runescape.wiki/w/Crystal_staff_(attuned) diff --git a/src/main/java/com/attacktimer/Spellbook.java b/src/main/java/com/attacktimer/Spellbook.java index e9f63af..89533c7 100644 --- a/src/main/java/com/attacktimer/Spellbook.java +++ b/src/main/java/com/attacktimer/Spellbook.java @@ -27,7 +27,7 @@ import com.google.common.collect.ImmutableMap; -enum Spellbook +public enum Spellbook { STANDARD(0), ANCIENT(1), diff --git a/src/main/java/com/attacktimer/VariableSpeed/BloodMoonSet.java b/src/main/java/com/attacktimer/VariableSpeed/BloodMoonSet.java index 6c06ec5..e3ad62e 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/BloodMoonSet.java +++ b/src/main/java/com/attacktimer/VariableSpeed/BloodMoonSet.java @@ -28,18 +28,18 @@ import com.attacktimer.AnimationData; import com.attacktimer.AttackProcedure; import net.runelite.api.Client; -import net.runelite.api.events.GameTick; public class BloodMoonSet implements IVariableSpeed { private static final int BLOOD_MOON_SET_ANIM_ID = 2792; - public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed, final int curSpeed) + + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final int damageDealt, final int lastSpecDelta, final int baseSpeed, final int curSpeed) { if (client.getLocalPlayer().hasSpotAnim(BLOOD_MOON_SET_ANIM_ID)) { - return curSpeed-1; + return curSpeed - 1; } return curSpeed; } - public void onGameTick(Client client, GameTick tick) {} } diff --git a/src/main/java/com/attacktimer/VariableSpeed/EyeOfAyak.java b/src/main/java/com/attacktimer/VariableSpeed/EyeOfAyak.java index c78d314..50ddc57 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/EyeOfAyak.java +++ b/src/main/java/com/attacktimer/VariableSpeed/EyeOfAyak.java @@ -28,11 +28,11 @@ import com.attacktimer.AnimationData; import com.attacktimer.AttackProcedure; import net.runelite.api.Client; -import net.runelite.api.events.GameTick; public class EyeOfAyak implements IVariableSpeed { - public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed, final int curSpeed) + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final int damageDealt, final int lastSpecDelta, final int baseSpeed, final int curSpeed) { // https://oldschool.runescape.wiki/w/Eye_of_ayak#Charged // https://oldschool.runescape.wiki/w/Eye_of_ayak#Special_attack @@ -46,5 +46,4 @@ public int apply(final Client client, final AnimationData curAnimation, final At } return curSpeed; } - public void onGameTick(Client client, GameTick tick) {} } diff --git a/src/main/java/com/attacktimer/VariableSpeed/IVariableSpeed.java b/src/main/java/com/attacktimer/VariableSpeed/IVariableSpeed.java index 0081977..f3ee74d 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/IVariableSpeed.java +++ b/src/main/java/com/attacktimer/VariableSpeed/IVariableSpeed.java @@ -1,7 +1,7 @@ package com.attacktimer.VariableSpeed; /* - * Copyright (c) 2024, Lexer747 + * Copyright (c) 2024-2026, Lexer747 * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -27,35 +27,38 @@ import com.attacktimer.AnimationData; import com.attacktimer.AttackProcedure; +import com.attacktimer.VariableSpeed.State.IStateTracker; import net.runelite.api.Client; -import net.runelite.api.events.GameTick; -public interface IVariableSpeed +public interface IVariableSpeed extends IStateTracker { /** * apply is the general method the attack timer plugin will call with all the data about an attack * currently being triggered. apply is only called when the plugin is sure the player just started - * attacking and hence a new cooldown. + * attacking and hence a new cooldown. to ensure a new IVariableSpeed implementation is used ensure + * to add a new instance of that class in {@see VariableSpeed.toApply}. * - * to ensure a new IVariableSpeed implementation is used ensure to add a new instance of that class in - * {@see VariableSpeed.toApply}. - * @param client the RuneScape client. - * @param curAnimation the animation currently being used to attack. - * @param atkType the overarching "attack type" for this attack, this is based on all the inference made - * about manual casts, etc. For more details about the attack {@see com.attacktimer.ClientUtils.Utils}. - * @param baseSpeed the speed at which the attack speed started before any other variable speeds have - * changed it. - * @param curSpeed the current speed at which the attack is now after variable speeds have been applied, - * e.g. rapid with range style. + * @param client the RuneScape client. + * @param curAnimation the animation currently being used to attack. + * @param atkType the overarching "attack type" for this attack, this is based on all the + * inference made about manual casts, etc. For more details about the attack + * {@see com.attacktimer.ClientUtils.Utils}. + * @param damageDealt the amount of damage being dealt by this attack (still WIP) + * @param lastSpecDelta the amount of spec consumed or generated by this attack (in 100s of percent, + * so consuming 25% spec would be -250.) + * @param baseSpeed the speed at which the attack speed started before any other variable speeds + * have changed it. + * @param curSpeed the current speed at which the attack is now after variable speeds have been + * applied, e.g. rapid with range style. * @return the new attack speed if the pre-conditions for this variable attack speed where met. */ - public int apply(Client client, AnimationData curAnimation, AttackProcedure atkType, int baseSpeed, int curSpeed); - /** - * onGameTick is pseudo subscription method, a variable speed implementation can implement this if the - * condition for the variable speed requires some larger state tracking and cannot be implemented in apply - * alone. - * @param client the RuneScape client. - * @param tick the current tick. - */ - public void onGameTick(Client client, GameTick tick); + public int apply( + final Client client, + final AnimationData curAnimation, + final AttackProcedure atkType, + final int damageDealt, + final int lastSpecDelta, + final int baseSpeed, + final int curSpeed + ); } \ No newline at end of file diff --git a/src/main/java/com/attacktimer/VariableSpeed/Leagues4and5.java b/src/main/java/com/attacktimer/VariableSpeed/Leagues4and5.java deleted file mode 100644 index 9decc6d..0000000 --- a/src/main/java/com/attacktimer/VariableSpeed/Leagues4and5.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.attacktimer.VariableSpeed; - -/* - * Copyright (c) 2022, Nick Graves - * Copyright (c) 2024, 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.AttackStyle; -import com.attacktimer.ClientUtils.Utils; -import net.runelite.api.Client; -import net.runelite.api.Varbits; -import net.runelite.api.WorldType; -import net.runelite.api.events.GameTick; - -public class Leagues4and5 implements IVariableSpeed -{ - public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed, final int curSpeed) - { - if (!client.getWorldType().contains(WorldType.SEASONAL)) - { - return curSpeed; - } - - AttackStyle attackStyle = Utils.getAttackStyle(client); - if (attackStyle == AttackStyle.RANGING || attackStyle == AttackStyle.LONGRANGE) - { - return applyLeagueFormulaSpeed(baseSpeed, client.getVarbitValue(Varbits.LEAGUES_RANGED_COMBAT_MASTERY_LEVEL)); - } - if (attackStyle == AttackStyle.ACCURATE || - attackStyle == AttackStyle.AGGRESSIVE || - attackStyle == AttackStyle.CONTROLLED || - attackStyle == AttackStyle.DEFENSIVE) - { - return applyLeagueFormulaSpeed(baseSpeed, client.getVarbitValue(Varbits.LEAGUES_MELEE_COMBAT_MASTERY_LEVEL)); - } - if (attackStyle == AttackStyle.CASTING || attackStyle == AttackStyle.DEFENSIVE_CASTING) - { - return applyLeagueFormulaSpeed(baseSpeed, client.getVarbitValue(Varbits.LEAGUES_MAGIC_COMBAT_MASTERY_LEVEL)); - } - - return curSpeed; - } - - private int applyLeagueFormulaSpeed(int baseSpeed, int masteryLevel) - { - // Older league's had no masteries and were all: "attack rate set to 50%, rounded down for 5t and above, rounded up below 4t. " - if (masteryLevel >= 5 || masteryLevel <= 0) - { - if (baseSpeed >= 4) - { - return baseSpeed / 2; - } - else - { - return (baseSpeed + 1) / 2; - } - } - else if (masteryLevel >= 3) - { - // "attack rate set to 80%, rounding down." e.g. https://oldschool.runescape.wiki/w/Melee_III - return (int) Math.floor(((double) baseSpeed) * 0.8); - } - return baseSpeed; - } - - public void onGameTick(Client client, GameTick tick) {} -} diff --git a/src/main/java/com/attacktimer/VariableSpeed/PurgingStaffSpec.java b/src/main/java/com/attacktimer/VariableSpeed/PurgingStaffSpec.java new file mode 100644 index 0000000..d54bae3 --- /dev/null +++ b/src/main/java/com/attacktimer/VariableSpeed/PurgingStaffSpec.java @@ -0,0 +1,89 @@ +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.ClientUtils.Utils; +import com.attacktimer.VariableSpeed.State.Yama; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.gameval.ItemID; + +public class PurgingStaffSpec implements IVariableSpeed +{ + private static final int PURGING_STAFF_ID = ItemID.PURGING_STAFF; + + private Yama yama; + public NPC lastTarget; + + PurgingStaffSpec(Yama yama) + { + this.yama = yama; + } + + // https://oldschool.runescape.wiki/w/Purging_staff#Special_attack + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final int damageDealt, final int lastSpecDelta, final int baseSpeed, final int curSpeed) + { + // For now the plugin only works for yama + if (!this.yama.inYamaRegion) + { + return curSpeed; + } + var target = Utils.getTargetNPC(client); + var flare = Yama.isEitherVoidFlare(target, lastTarget); + lastTarget = target; + if (flare == null) + { + return curSpeed; + } + if (yama == null) + { + return curSpeed; + } + + yama.dealVoidFlareDamage(flare, damageDealt); + if (lastSpecDelta != -250) + { + // not using the spec + return curSpeed; + } + if (Utils.getWeaponId(client) != PURGING_STAFF_ID) + { + // not using a purging staff + return curSpeed; + } + + if (yama.getVoidFlareDead(flare)) + { + // speed up! + // according to the wiki this should be 3 ticks but is actually 2 ticks is normal circumstances + return curSpeed - 2; + } + return curSpeed; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/RapidAttackStyle.java b/src/main/java/com/attacktimer/VariableSpeed/RapidAttackStyle.java index 3109531..7d990a2 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/RapidAttackStyle.java +++ b/src/main/java/com/attacktimer/VariableSpeed/RapidAttackStyle.java @@ -32,20 +32,19 @@ import com.attacktimer.ClientUtils.Utils; import net.runelite.api.Client; import net.runelite.api.VarPlayer; -import net.runelite.api.events.GameTick; public class RapidAttackStyle implements IVariableSpeed { - public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed, final int curSpeed) + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final int damageDealt, final int lastSpecDelta, final int baseSpeed, final int curSpeed) { // index 1 == rapid final boolean isRapid = client.getVarpValue(VarPlayer.ATTACK_STYLE) == 1; - if (atkProcedure == AttackProcedure.MELEE_OR_RANGE && Utils.getAttackStyle(client) == AttackStyle.RANGING && isRapid) + if (atkType == AttackProcedure.MELEE_OR_RANGE && Utils.getAttackStyle(client) == AttackStyle.RANGING && isRapid) { // Also works for salamanders which attack 1 tick faster when using the ranged style - return curSpeed-1; + return curSpeed - 1; } return curSpeed; } - public void onGameTick(Client client, GameTick tick) {} } diff --git a/src/main/java/com/attacktimer/VariableSpeed/RedKerisSpec.java b/src/main/java/com/attacktimer/VariableSpeed/RedKerisSpec.java index a0e5d93..65fd5b8 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/RedKerisSpec.java +++ b/src/main/java/com/attacktimer/VariableSpeed/RedKerisSpec.java @@ -28,18 +28,17 @@ import com.attacktimer.AnimationData; import com.attacktimer.AttackProcedure; import net.runelite.api.Client; -import net.runelite.api.events.GameTick; public class RedKerisSpec implements IVariableSpeed { - public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed, final int curSpeed) + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final int damageDealt, final int lastSpecDelta, final int baseSpeed, final int curSpeed) { if (curAnimation == AnimationData.MELEE_RED_KERIS_SPEC) { // TODO add miss/hit tracking code, if we missed this delay is not applied - return curSpeed+4; + return curSpeed + 4; } return curSpeed; } - public void onGameTick(Client client, GameTick tick) {} } diff --git a/src/main/java/com/attacktimer/VariableSpeed/Scurrius.java b/src/main/java/com/attacktimer/VariableSpeed/Scurrius.java index a3dbe64..e6b1e6a 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/Scurrius.java +++ b/src/main/java/com/attacktimer/VariableSpeed/Scurrius.java @@ -30,7 +30,6 @@ import com.attacktimer.ClientUtils.Utils; import net.runelite.api.Client; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameTick; /** * Scurrius: https://oldschool.runescape.wiki/w/Scurrius/Strategies#Strategies @@ -62,7 +61,8 @@ private static boolean attackingGiantRatWithBoneWeapon(final int equipped, final return correctWeapon && correctCoords && correctRegion && correctEnemy; } - public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed, final int curSpeed) + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final int damageDealt, final int lastSpecDelta, final int baseSpeed, final int curSpeed) { final WorldPoint location = Utils.getLocation(client); final int weaponId = Utils.getWeaponId(client); @@ -73,5 +73,4 @@ public int apply(final Client client, final AnimationData curAnimation, final At } return curSpeed; } - public void onGameTick(final Client client, final GameTick tick) {} } diff --git a/src/main/java/com/attacktimer/VariableSpeed/ShadowCrash.java b/src/main/java/com/attacktimer/VariableSpeed/ShadowCrash.java new file mode 100644 index 0000000..855f06d --- /dev/null +++ b/src/main/java/com/attacktimer/VariableSpeed/ShadowCrash.java @@ -0,0 +1,312 @@ +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.VariableSpeed.State.MarkOfDarkness; +import com.attacktimer.VariableSpeed.State.TickCount; +import com.attacktimer.VariableSpeed.State.Yama; +import com.attacktimer.VariableSpeed.State.YamaPhase; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import net.runelite.api.Client; +import net.runelite.api.GraphicsObject; +import net.runelite.api.Player; +import net.runelite.api.WorldView; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; + +public class ShadowCrash +{ + private static final int FIREBALL_ID = 3262; + // The unit of a LocalPoint is 1/128th of a tile + private static final int TILE = 128; + + // Each fireball is 3 tiles apart when vertical or horizontal, diagonal are shorter. See + // shadowCrash.png. + private static final int HV_GAP_1 = 3; + private static final int HV_GAP_2 = 6; + private static final int HV_DELTA_1 = HV_GAP_1 * TILE; + private static final int HV_DELTA_2 = HV_GAP_2 * TILE; + private static final int D_GAP_1 = 2; + private static final int D_GAP_2 = 4; + private static final int D_DELTA_1 = D_GAP_1 * TILE; + private static final int D_DELTA_2 = D_GAP_2 * TILE; + + // externally tracked state + private TickCount tickCount; + private Yama yama; + private MarkOfDarkness mod; + + // local state + + // when was the speed up consumed in ticks + private int consumed = -1; + private Map fireballs; + private ArrayList lines; + private WorldPoint player; + + ShadowCrash(Yama yama, MarkOfDarkness mod, TickCount tc) + { + this.yama = yama; + this.mod = mod; + this.tickCount = tc; + this.fireballs = new HashMap<>(); + this.lines = new ArrayList<>(); + } + + // https://oldschool.runescape.wiki/w/Yama/Strategies#Phase_3 + // + // If the player only moves one tile away from the crash and has Mark of Darkness active, their next + // attack will be used one tick faster than usual. Each crash is individual to the player themselves and + // they will not take damage from another player's crash. + // + // This function will adjust the attackDelayHoldoffTicks if the player successfully dodges the shadowcrash + // in the sweet spot. + public int onRender(final Client client, final int attackDelayHoldoffTicks, final boolean isUsingMagic) + { + // if mark of darkness isn't active then no speed up can be gained. + if (!yama.inYamaRegion || yama.phase() != YamaPhase.P3 || !mod.isActive()) + { + return 0; + } + if (!tickCount.isWithinNTicks(consumed, 2) && shadowCrashSweetSpot(client)) + { + // the player is in the sweet spot with MoD active so the cooldown of whatever they used is now + // improved by a tick. + consumed = tickCount.get(); + // theres a bug/feature in the yama mechanic, if you are using a mage weapon then the speed up + // doesn't apply if the global cooldown is 2 or less. It might also affect range but no-one uses + // that at yama so IDC. Another reason to prefer melee P3. + if (attackDelayHoldoffTicks <= 2 && isUsingMagic) + { + return 0; + } + return -1; + } + return 0; + } + + private boolean shadowCrashSweetSpot(final Client client) + { + getState(client); + // if there's not lines on screen then we can't be in the sweet spot yet + if (lines.size() <= 0) + { + return false; + } + + for (var line : lines) + { + if (line.isActive()) + { + // there can only be one active line at once + return line.inSweetSpot(client, player); + } + } + return false; + } + + private void getState(final Client client) + { + fireballs.clear(); + lines.clear(); + final WorldView tlwv = client.getTopLevelWorldView(); + final Player localPlayer = client.getLocalPlayer(); + final WorldView playerWv = localPlayer.getWorldView(); + getFireballs(tlwv); + if (playerWv != tlwv) + { + getFireballs(playerWv); + } + if (fireballs.size() < 3) + { + // don't compute further we should wait for at least 3 fireballs to be on screen + return; + } + player = localPlayer.getWorldLocation(); + lines = getLines(); + } + + private void getFireballs(final WorldView wv) + { + for (GraphicsObject graphicsObject : wv.getGraphicsObjects()) + { + if (graphicsObject.getId() == FIREBALL_ID) + { + fireballs.put(graphicsObject.getLocation(), graphicsObject); + } + } + } + + private ArrayList getLines() + { + final var result = new ArrayList(); + + while (fireballs.size() >= 3) + { + // fireballs can be in exactly 4 orientations (when accounting for symmetry): vertical, + // horizontal, diagonal. We start by getting the lexicographically smallest fireball (x first). + LocalPoint first = null; + final Set fireballsLocations = fireballs.keySet(); + for (var location : fireballsLocations) + { + if (first == null) + { + first = location; + } + else + { + if (first.getX() > location.getX()) + { + first = location; + } + else if (first.getX() == location.getX()) + { + if (first.getY() > location.getY()) + { + first = location; + } + } + } + } + if (first == null) + { + return result; + } + // We have found the minimum fireball in the x and y + + // See shadowCrash.png + // + // This means we have exactly 4 cases to check for + // * Increasing X -> is vertical line ending at D1 + // * Increasing Y -> is a horizontal line ending at 2 + // * Increasing X & Y -> is a diagonal (D2) as it ends at D2 + // * Increasing X, Decreasing Y -> is a diagonal (D1) + // + // This only works if the first point is actually the smallest x first, then smallest y. + + var centre = first.plus(D_DELTA_1, D_DELTA_1); + var end = first.plus(D_DELTA_2, D_DELTA_2); + if (fireballsLocations.contains(centre) && fireballsLocations.contains(end)) + { + result.add(new FireballLine(LineType.D2, centre, fireballs.get(centre))); + removeLine(first, centre, end); + continue; + } + + centre = first.plus(HV_DELTA_1, 0); + end = first.plus(HV_DELTA_2, 0); + if (fireballsLocations.contains(centre) && fireballsLocations.contains(end)) + { + result.add(new FireballLine(LineType.H, centre, fireballs.get(centre))); + removeLine(first, centre, end); + continue; + } + + centre = first.plus(0, HV_DELTA_1); + end = first.plus(0, HV_DELTA_2); + if (fireballsLocations.contains(centre) && fireballsLocations.contains(end)) + { + result.add(new FireballLine(LineType.V, centre, fireballs.get(centre))); + removeLine(first, centre, end); + continue; + } + + centre = first.plus(D_DELTA_1, -D_DELTA_1); + end = first.plus(D_DELTA_2, -D_DELTA_2); + if (fireballsLocations.contains(centre) && fireballsLocations.contains(end)) + { + result.add(new FireballLine(LineType.D1, centre, fireballs.get(centre))); + removeLine(first, centre, end); + continue; + } + // we do reach this occasionally (no valid line found) and it's when some of the set of 3 has + // spawned but not all 3, in which case we should just return early. + return result; + } + return result; + } + + private void removeLine(final LocalPoint a, final LocalPoint b, final LocalPoint c) + { + fireballs.remove(a); + fireballs.remove(b); + fireballs.remove(c); + } + + public static boolean eq(final WorldPoint t, final int dx, final int dy, final WorldPoint other) + { + return t.getX() + dx == other.getX() && t.getY() + dy == other.getY(); + } + + class FireballLine + { + private LineType type; + private LocalPoint centre; + private GraphicsObject cObject; + + FireballLine(final LineType type, final LocalPoint centre, final GraphicsObject cObject) + { + this.centre = centre; + this.cObject = cObject; + this.type = type; + } + + public boolean isActive() + { + // The entire fireball animation is 40 frames, crashes and tick speed up occur between frames: 30 + // and 40. There is 10 animation frames per tick. + final int animationFrame = cObject.getAnimationFrame(); + return animationFrame >= 30 && animationFrame <= 40; + } + + public boolean inSweetSpot(final Client client, final WorldPoint lp) + { + final var wp = WorldPoint.fromLocal(client, centre); + // See shadowCrash.png | each line type has two valid sweet spots, +- one tile from the centre + // where the exact tiles are defined by which line type it is. + switch (this.type) + { + case V: + return eq(wp, 1, 0, lp) || eq(wp, -1, 0, lp); + case H: + return eq(wp, 0, 1, lp) || eq(wp, 0, -1, lp); + case D1: + return eq(wp, 1, 1, lp) || eq(wp, -1, -1, lp); + case D2: + return eq(wp, -1, 1, lp) || eq(wp, 1, -1, lp); + } + return false; + } + } + + enum LineType + { + V, H, D1, D2; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/State/IStateTracker.java b/src/main/java/com/attacktimer/VariableSpeed/State/IStateTracker.java new file mode 100644 index 0000000..9c5f4d2 --- /dev/null +++ b/src/main/java/com/attacktimer/VariableSpeed/State/IStateTracker.java @@ -0,0 +1,53 @@ +package com.attacktimer.VariableSpeed.State; + +/* + * 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 net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameTick; + +public interface IStateTracker +{ + /** + * onGameTick is a subscription method, an implementation can implement this if the condition for + * the variable speed requires some larger state tracking and cannot be implemented in apply alone. + * + * @param client the RuneScape client. + * @param tick the current tick. + */ + default public void onGameTick(final Client client, final GameTick tick) + {}; + + /** + * onChatMessage is a subscription method, an implementation can implement this if the condition for + * the variable speed requires some larger state tracking and cannot be implemented in apply alone. + * + * @param client the RuneScape client. + * @param event the chat message event. + */ + default public void onChatMessage(final Client client, final ChatMessage event) + {}; +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/State/MarkOfDarkness.java b/src/main/java/com/attacktimer/VariableSpeed/State/MarkOfDarkness.java new file mode 100644 index 0000000..e89d37c --- /dev/null +++ b/src/main/java/com/attacktimer/VariableSpeed/State/MarkOfDarkness.java @@ -0,0 +1,89 @@ +package com.attacktimer.VariableSpeed.State; + +/* + * 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 java.time.Duration; +import java.time.Instant; +import net.runelite.api.Client; +import net.runelite.api.Skill; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.gameval.ItemID; +import net.runelite.client.util.RSTimeUnit; + +/** + * MarkOfDarkness tracks when mark of darkness was cast and how long it will last, it is queryable + * via `isActive`. + */ +public class MarkOfDarkness implements IStateTracker +{ + private static final String MARK_OF_DARKNESS_MESSAGE = "You have placed a Mark of Darkness upon yourself."; + + private Instant modStartTime = Instant.now(); + private Instant modEndTime = Instant.now(); + + public void onChatMessage(Client client, ChatMessage event) + { + final String message = event.getMessage(); + if (message.endsWith(MARK_OF_DARKNESS_MESSAGE)) + { + final Duration duration = getMarkOfDarknessDuration(client); + this.modStartTime = Instant.now(); + this.modEndTime = modStartTime.plus(duration); + } + } + + /** + * returns true if and only if mark of darkness is currently active on the player. The only edge + * case is if the plugin was disabled whilst mark of darkness is active. + */ + public boolean isActive() + { + final var n = Instant.now(); + return this.modStartTime.isBefore(n) && this.modEndTime.isAfter(n); + } + + // Taken from the timers and buffs plugins (as of commit: + // https://github.com/runelite/runelite/commit/9a6f7017ed9c9b8ea2687b0d84ce79a28434cefd) same + // license as this file. + // + // # Credits: + // + // https://github.com/runelite/runelite/commits?author=Psychemaster + // https://github.com/runelite/runelite/commits?author=YvesW + // https://github.com/runelite/runelite/commits?author=Nightfirecat + private static Duration getMarkOfDarknessDuration(Client client) + { + final int magicLevel = client.getRealSkillLevel(Skill.MAGIC); + final Duration markOfDarknessDuration = Duration.of((long) magicLevel * 3, RSTimeUnit.GAME_TICKS); + + if (Utils.getWeaponId(client) == ItemID.PURGING_STAFF) + { + return markOfDarknessDuration.multipliedBy(5); + } + return markOfDarknessDuration; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java b/src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java new file mode 100644 index 0000000..20eff49 --- /dev/null +++ b/src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java @@ -0,0 +1,60 @@ +package com.attacktimer.VariableSpeed.State; + +/* + * 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 net.runelite.api.events.GameTick; + +/** + * TickCount increments on each game tick. Due to 6 hour logs this cannot overflow or wrap around. + */ +public class TickCount implements IStateTracker +{ + private int tickCount; + + public int get() + { + return tickCount; + } + + public void onGameTick(Client client, GameTick tick) + { + tickCount++; + } + + /** + * isWithinNTicks returns true if the current game tick count is within N ticks of the argument + * `toCheckAgainst` + * + * @param toCheckAgainst some fixed point that occurred in the past (in game ticks) + * @param N the number of ticks of generosity + * @return true if the current counter is N or less ticks away from `toCheckAgainst` + */ + public boolean isWithinNTicks(int toCheckAgainst, int N) + { + return this.tickCount <= toCheckAgainst + N && this.tickCount >= toCheckAgainst; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/State/Yama.java b/src/main/java/com/attacktimer/VariableSpeed/State/Yama.java new file mode 100644 index 0000000..52c7fe3 --- /dev/null +++ b/src/main/java/com/attacktimer/VariableSpeed/State/Yama.java @@ -0,0 +1,202 @@ +package com.attacktimer.VariableSpeed.State; + +/* + * 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 java.util.HashMap; +import java.util.Map; +import lombok.NonNull; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameTick; + +/** + * Yama tracks various bits of state related to the boss https://oldschool.runescape.wiki/w/Yama. + */ +public class Yama implements IStateTracker +{ + private static final int HP_FUDGE = 2; + + private static final int VOID_FLARE_HP_P1_P2 = 140 - HP_FUDGE; + private static final int VOID_FLARE_HP_P3 = 71 - HP_FUDGE; + private static final int VOID_FLARE_ID = 14179; + + private static final int YAMA_REGION_ID = 6045; + private static final int YAMA_ID = 14176; + private static final int YAMA_PHASE_TRANSITION_ANIMATION_ID = 12147; + + // null if yama isn't alive. + public YamaData yama; + public boolean inYamaRegion; + public NPC lastTarget; + + public void onGameTick(Client client, GameTick tick) + { + inYamaRegion = Utils.isInRegionId(client, YAMA_REGION_ID); + if (!inYamaRegion) + { + yama = null; + return; + } + if (yama != null) + { + yama.determineYamaPhase(); + return; + } + for (NPC npc : client.getTopLevelWorldView().npcs()) + { + if (npc.getId() == YAMA_ID) + { + yama = new YamaData(npc); + return; + } + } + } + + public void onChatMessage(Client client, ChatMessage event) + { + if (!inYamaRegion || yama == null) + { + return; + } + if (event.getMessage().startsWith("Your Yama success count is:")) + { + yama.killed(); + } + } + + public static NPC isEitherVoidFlare(NPC a, NPC b) + { + if (a != null && a.getId() == VOID_FLARE_ID) + { + return a; + } + + if (b != null && b.getId() == VOID_FLARE_ID) + { + return b; + } + return null; + } + + public void dealVoidFlareDamage(NPC target, int damageDealt) + { + if (yama == null) + { + return; + } + this.yama.dealVoidFlareDamage(target, damageDealt); + } + + public boolean getVoidFlareDead(NPC target) + { + if (yama == null) + { + return false; + } + return this.yama.getVoidFlareDead(target); + } + + public YamaPhase phase() + { + if (yama == null) + { + return YamaPhase.P1; + } + return this.yama.phase; + } + + class YamaData + { + @NonNull + private NPC yama; + private YamaPhase phase; + private int phaseChangeCooldown; + private Map voidFlares; + + YamaData(NPC yama) + { + this.yama = yama; + this.phase = YamaPhase.P1; + this.phaseChangeCooldown = 0; + this.voidFlares = new HashMap(); + } + + public boolean getVoidFlareDead(NPC target) + { + if (!this.voidFlares.containsKey(target)) + { + return false; + } + return this.voidFlares.get(target) <= 0; + } + + public void dealVoidFlareDamage(NPC target, int damageDealt) + { + if (this.voidFlares.containsKey(target)) + { + this.voidFlares.put(target, this.voidFlares.get(target) - damageDealt); + } + else + { + final var hp = this.getVoidFlareHp() - damageDealt; + this.voidFlares.put(target, hp); + } + } + + private void determineYamaPhase() + { + if (phaseChangeCooldown == 0 && this.yama.getAnimation() == YAMA_PHASE_TRANSITION_ANIMATION_ID) + { + this.phaseChangeCooldown = 20; + this.phase = this.phase.nextPhase(); + } + if (this.phaseChangeCooldown > 0) + { + this.phaseChangeCooldown--; + } + } + + private int getVoidFlareHp() + { + switch (this.phase) + { + case P1: // fallthrough + case P2: + return VOID_FLARE_HP_P1_P2; + default: + return VOID_FLARE_HP_P3; + } + } + + private void killed() + { + this.phase = YamaPhase.P1; + this.voidFlares.clear(); + } + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/State/YamaPhase.java b/src/main/java/com/attacktimer/VariableSpeed/State/YamaPhase.java new file mode 100644 index 0000000..79bb4dd --- /dev/null +++ b/src/main/java/com/attacktimer/VariableSpeed/State/YamaPhase.java @@ -0,0 +1,45 @@ +package com.attacktimer.VariableSpeed.State; + +/* + * 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. + */ + +public enum YamaPhase +{ + P1, P2, P3; + + public YamaPhase nextPhase() + { + switch (this) + { + case P1: + return P2; + case P2: + return P3; + case P3: + return P3; + } + return P1; + } +} \ No newline at end of file diff --git a/src/main/java/com/attacktimer/VariableSpeed/TombsOfAmascut.java b/src/main/java/com/attacktimer/VariableSpeed/TombsOfAmascut.java index 8277f49..26c95fe 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/TombsOfAmascut.java +++ b/src/main/java/com/attacktimer/VariableSpeed/TombsOfAmascut.java @@ -30,7 +30,6 @@ import com.attacktimer.AttackType; import com.attacktimer.ClientUtils.Utils; import net.runelite.api.Client; -import net.runelite.api.events.GameTick; /** * There is no cooldown when attacking the skulls with melee. @@ -41,7 +40,8 @@ public class TombsOfAmascut implements IVariableSpeed // https://oldschool.runescape.wiki/w/Energy_Siphon private static final int ENERGY_SIPHON_ID = 11772; - public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed, final int curSpeed) + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final int damageDealt, final int lastSpecDelta, final int baseSpeed, final int curSpeed) { final int targetId = Utils.getTargetId(client); final AttackType attkType = Utils.getAttackType(client); @@ -51,5 +51,4 @@ public int apply(final Client client, final AnimationData curAnimation, final At } return curSpeed; } - public void onGameTick(Client client, GameTick tick) {} } diff --git a/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java b/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java index 98ec413..e3d732f 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java +++ b/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java @@ -29,6 +29,7 @@ import com.attacktimer.AttackProcedure; import com.attacktimer.AttackType; import com.attacktimer.ClientUtils.Utils; +import com.attacktimer.VariableSpeed.State.TickCount; import com.attacktimer.WeaponType; import java.util.ArrayList; import java.util.HashMap; @@ -56,7 +57,15 @@ */ public class TormentedDemons implements IVariableSpeed { - public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed, final int curSpeed) + private TickCount tickCount; + + TormentedDemons(TickCount tc) + { + this.tickCount = tc; + } + + public int apply(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final int damageDealt, final int lastSpecDelta, final int baseSpeed, final int curSpeed) { int targetId = Utils.getTargetId(client); if (!isTormentedDemon(targetId)) @@ -70,18 +79,18 @@ public int apply(final Client client, final AnimationData curAnimation, final At return curSpeed; } final DemonData targetDemon = tormentedDemons.get(target); - if (!targetDemon.isVulnerable(tickCount)) + if (!targetDemon.isVulnerable(tickCount.get())) { return curSpeed; } // The demon is vulnerable! However if it's already been attacked in a previous tick then the // vulnerability is moot. - if (targetDemon.vulnConsumed(tickCount)) + if (targetDemon.vulnConsumed(tickCount.get())) { return curSpeed; } // Finally the last checks, only certain attack styles and weapons can trigger the effect. - switch (atkProcedure) + switch (atkType) { case POWERED_STAVE: // Powered staves cannot trigger the effect @@ -123,12 +132,10 @@ private static boolean isTormentedDemon(int targetId) return targetId == TORMENTED_DEMON_ID || targetId == TORMENTED_DEMON_2_ID; } - private Map tormentedDemons = new HashMap();; - private int tickCount; + private Map tormentedDemons = new HashMap(); public void onGameTick(Client client, GameTick tick) { - tickCount++; for (NPC npc : client.getTopLevelWorldView().npcs()) { if (!isTormentedDemon(npc.getId())) @@ -139,20 +146,20 @@ public void onGameTick(Client client, GameTick tick) if (tormentedDemons.containsKey(npc)) { DemonData d = tormentedDemons.get(npc); - d.update(tickCount, isVulnerable); + d.update(tickCount.get(), isVulnerable); } else { - tormentedDemons.put(npc, new DemonData(tickCount, isVulnerable)); + tormentedDemons.put(npc, new DemonData(tickCount.get(), isVulnerable)); } } // Only check for staleness every so often - if (tickCount % 100 == 0) + if (tickCount.get() % 100 == 0) { var toDelete = new ArrayList(); for (Entry td : tormentedDemons.entrySet()) { - if (td.getValue().isStale(tickCount)) + if (td.getValue().isStale(tickCount.get())) { toDelete.add(td.getKey()); } @@ -177,6 +184,7 @@ class DemonData private Integer vulnerableStart; private Integer vulnerableFinish; private int attacked; + DemonData(int tick, boolean vuln) { lastSpotted = tick; @@ -229,7 +237,6 @@ boolean isStale(int tick) return false; } - // vulnConsumed returns true if the demon was already attack, false if not and the vuln is still usable boolean vulnConsumed(int tick) { diff --git a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java index 21f3ede..c4442f1 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java +++ b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java @@ -27,7 +27,12 @@ import com.attacktimer.AnimationData; import com.attacktimer.AttackProcedure; +import com.attacktimer.VariableSpeed.State.IStateTracker; +import com.attacktimer.VariableSpeed.State.MarkOfDarkness; +import com.attacktimer.VariableSpeed.State.TickCount; +import com.attacktimer.VariableSpeed.State.Yama; import net.runelite.api.Client; +import net.runelite.api.events.ChatMessage; import net.runelite.api.events.GameTick; public class VariableSpeed @@ -35,45 +40,73 @@ public class VariableSpeed /** * computeSpeed will forward the client, animation data and current weapon speed to all the known classes * which can affect the base speed of a weapon. See implementations of IVariableSpeed. - * @param client - * @param curAnimation - * @param atkType - * @param baseSpeed - * @return */ - public static int computeSpeed(final Client client, final AnimationData curAnimation, final AttackProcedure atkProcedure, final int baseSpeed) + public static int computeSpeed(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final int damageDealt, final int lastSpecDelta, final int baseSpeed) { int newSpeed = baseSpeed; for (IVariableSpeed i : TO_APPLY) { - newSpeed = i.apply(client, curAnimation, atkProcedure, baseSpeed, newSpeed); + newSpeed = i.apply(client, curAnimation, atkType, damageDealt, lastSpecDelta, baseSpeed, newSpeed); } return newSpeed; } - public static void onGameTick(Client client, GameTick tick) + public static void onGameTick(final Client client, final GameTick tick) { - for (IVariableSpeed i : TO_APPLY) + for (IStateTracker i : TO_TRACK) + { + i.onGameTick(client, tick); + } + for (IStateTracker i : TO_APPLY) { i.onGameTick(client, tick); } } + public static void onChatMessage(final Client client, final ChatMessage event) + { + for (IStateTracker i : TO_TRACK) + { + i.onChatMessage(client, event); + } + for (IStateTracker i : TO_APPLY) + { + i.onChatMessage(client, event); + } + } + + private static final Yama YAMA = new Yama(); + private static final MarkOfDarkness MARK_OF_DARKNESS = new MarkOfDarkness(); + private static final TickCount TC = new TickCount(); + + 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. + TC, + YAMA, + MARK_OF_DARKNESS, + }; private static final IVariableSpeed[] TO_APPLY = { - // Order matters, apply leagues first, then any incremental modifications like rapid, or set effects. + // Order matters, apply leagues first, then any incremental modifications like rapid, or set + // effects. // Then overriding speeds last, which set a speed. - // new Leagues4and5(), // Incremental: new BloodMoonSet(), new RapidAttackStyle(), new RedKerisSpec(), + new PurgingStaffSpec(YAMA), new EyeOfAyak(), - new TormentedDemons(), + new TormentedDemons(TC), // Overriding modifiers: new Scurrius(), new TombsOfAmascut(), }; + // 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, TC); + } diff --git a/src/main/java/com/attacktimer/VariableSpeed/shadowCrash.png b/src/main/java/com/attacktimer/VariableSpeed/shadowCrash.png new file mode 100644 index 0000000000000000000000000000000000000000..1e50f62f79ba44af90142902f7a2e2506f3f91d6 GIT binary patch literal 22515 zcmYg%1yt0}7q3c(bft5>hy%F0Nnz&`h1y?U*Ij0pS7gkow4_VLKob+TEJUL^8o|Wc^ewoWm z1RjbCxVKQhthUg*wJ>oWR};^EJtJ><`Pb>esrMP_qP8l&_N@MDhIaW?%*0XoXi>@O z;hl@b;}#*d?jr#jceoCl3_+Y*OPS?$BiX+5$G1k4NBxlb1izItM>azij2;mVsmDGe z-i;oQNy9{Wc(@c)gpm-oe1h5@BNmc2yKQ(nI0N@T56`u=eO-8Jl=6V&ukKo0L{4AI zM?&u@{2ljyn{c1@K_=LA1ZarQ8##Bb_Q$_qcJT<}>3d|CbUD)LoFR$y~KXwy_IC}XD_?}D~<8@r4nKXJ*6*F-- zTZw>buDFiRFpX(MAw1;^0gfEeBrB`pA z*J2L*zU!S2Ovr3~Tw(CKAoO>Ab(MfI`u1aA-H+t0yNADC=rO`1J!%~aS7i9@zwc?V z^>jZQI|_lS!Zc~XYbl_^0r}{N7HzF$NOlqdZ92vsDJkzQDxE6uGwvcJ`k6ohys(C2weNGraHRAHK)? ztS4fj_oP2t+{{+JfA_!D6L#f(x(jBY7zj2Q;H>}vLgOPKSK{o~!DV5*{7|zA2x*1T zdi4CdjFH`7c)|nd?s5!N*Z$P05}9$k8o7()ZaG})^1vp*eK2r?i;|uiV?=FL4lI&neGqI-YbNpVt2hKOba0qli8%P6QQY`5v(~ z9$EbYt+o4--mbV3w%)jkP3~oSV^BVCAi3hY_}Qbe>p5-TK7S#&UH2zs{pfBrY(9Np z+qa3*fsB)}Ov;b2`Ys<293!|lBDkmAp`@Cv2`Xb3E!4|NVu9(x=4-#DH>q*gJG<59 z;Eu(O_1`UP!=Cl$vGsj=%Z7=FfpYruUD6YeC%2E|YzU6GNb3zH zkdtx^GNKz)00+4|EE>MC#>EYS$`N&Cni5Ur(Q f6~zppb)?Im14mF)R#yYL(r)2XhT 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 Date: Sun, 10 May 2026 22:29:57 +0100 Subject: [PATCH 2/2] Version bump --- build.gradle | 4 ++++ runelite-plugin.properties | 1 + 2 files changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index 51135c9..cdc61d2 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,11 @@ java { targetCompatibility = "1.11" } +def props = new Properties() +file('runelite-plugin.properties').withInputStream { props.load(it) } + group = 'com.attacktimer' +version = props.getProperty('version') tasks.withType(JavaCompile) { options.encoding = 'UTF-8' diff --git a/runelite-plugin.properties b/runelite-plugin.properties index 8e3ec2b..3057d42 100644 --- a/runelite-plugin.properties +++ b/runelite-plugin.properties @@ -1,6 +1,7 @@ displayName=AttackTimer author=ngraves95,Lexer747 build=standard +version=1.2.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