From d68900748f2ba34bc42cefb04e9def0838509de5 Mon Sep 17 00:00:00 2001 From: Lexer747 Date: Sun, 30 Aug 2026 13:30:55 +0100 Subject: [PATCH] add .gitattributes Convert everything to consistent line endings --- .gitattributes | 11 + .github/pull_request_template.md | 36 +- .github/workflows/ci.yml | 82 +-- config/checkstyle/checkstyle.xml | 78 +-- gradlew.bat | 178 ++--- .../java/com/attacktimer/AttackProcedure.java | 66 +- src/main/java/com/attacktimer/AttackType.java | 82 +-- .../com/attacktimer/ClientUtils/Utils.java | 354 +++++----- src/main/java/com/attacktimer/Damage.java | 222 +++--- .../attacktimer/VariableSpeed/Amoxliatl.java | 120 ++-- .../VariableSpeed/BloodMoonSet.java | 92 +-- .../attacktimer/VariableSpeed/EyeOfAyak.java | 102 +-- .../VariableSpeed/IVariableSpeed.java | 130 ++-- .../VariableSpeed/PurgingStaffSpec.java | 164 ++--- .../VariableSpeed/RapidAttackStyle.java | 104 +-- .../VariableSpeed/RedKerisSpec.java | 102 +-- .../VariableSpeed/RoyalTitans.java | 526 +++++++------- .../attacktimer/VariableSpeed/Scurrius.java | 152 ++--- .../VariableSpeed/ShadowCrash.java | 644 +++++++++--------- .../VariableSpeed/State/IStateTracker.java | 144 ++-- .../VariableSpeed/State/MarkOfDarkness.java | 178 ++--- .../VariableSpeed/State/TickCount.java | 122 ++-- .../attacktimer/VariableSpeed/State/Yama.java | 428 ++++++------ .../VariableSpeed/State/YamaPhase.java | 88 +-- .../VariableSpeed/TombsOfAmascut.java | 110 +-- .../VariableSpeed/TormentedDemons.java | 514 +++++++------- .../VariableSpeed/VariableSpeed.java | 280 ++++---- src/test/java/com/attacktimer/BasicTests.java | 356 +++++----- src/test/java/com/attacktimer/EatTest.java | 166 ++--- src/test/java/com/attacktimer/README.md | 68 +- .../java/com/attacktimer/RoyalTitansTest.java | 608 ++++++++--------- .../com/attacktimer/TormentedDemonsTest.java | 288 ++++---- src/test/java/com/attacktimer/Update.java | 144 ++-- 33 files changed, 3375 insertions(+), 3364 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b875a2b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +*.bat text eol=crlf +*.ps1 text eol=crlf + +*.gradle text eol=lf +*.java text eol=lf +*.md text eol=lf +*.properties text eol=lf +*.sh text eol=lf +*.txt text eol=lf +*.xml text eol=lf +*.yml text eol=lf \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9bbe422..c38aead 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,19 +1,19 @@ -## Summary - - - -## Testing - - + +## Testing + + \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 521b881..c284943 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,41 +1,41 @@ -name: CI - -on: [ push, pull_request ] - -jobs: - build: - runs-on: ubuntu-latest - - permissions: - contents: read - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 - with: - # should be kept in sync with [https://github.com/runelite/runelite/blob/master/.github/workflows/CI.yml] - java-version: '11' # runelite currently targets JDK v11 - distribution: 'temurin' - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 - - - name: Compile - run: ./gradlew compileJava - - - name: Compile Tests - run: ./gradlew compileTestJava - - - name: Lint & Formatting - run: ./gradlew checkstyleMain checkstyleTest - - - name: Run Tests - run: ./gradlew test - - - name: Run Pre-PluginHub-Checks - run: | - longestLine=$(cat build.gradle | wc -L) - if (( longestLine > 120 )); then - echo "Malformed gradle - longest line: ${longestLine}" - exit 1 - fi +name: CI + +on: [ push, pull_request ] + +jobs: + build: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + # should be kept in sync with [https://github.com/runelite/runelite/blob/master/.github/workflows/CI.yml] + java-version: '11' # runelite currently targets JDK v11 + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0 + + - name: Compile + run: ./gradlew compileJava + + - name: Compile Tests + run: ./gradlew compileTestJava + + - name: Lint & Formatting + run: ./gradlew checkstyleMain checkstyleTest + + - name: Run Tests + run: ./gradlew test + + - name: Run Pre-PluginHub-Checks + run: | + longestLine=$(cat build.gradle | wc -L) + if (( longestLine > 120 )); then + echo "Malformed gradle - longest line: ${longestLine}" + exit 1 + fi diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 14f4622..ca5edbe 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -1,39 +1,39 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/com/attacktimer/AttackProcedure.java b/src/main/java/com/attacktimer/AttackProcedure.java index 12570a6..e30de57 100644 --- a/src/main/java/com/attacktimer/AttackProcedure.java +++ b/src/main/java/com/attacktimer/AttackProcedure.java @@ -1,33 +1,33 @@ -package com.attacktimer; - -/* - * 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. - */ - -public enum AttackProcedure -{ - POWERED_STAVE, - MANUAL_AUTO_CAST, - MELEE_OR_RANGE -} +package com.attacktimer; + +/* + * 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. + */ + +public enum AttackProcedure +{ + POWERED_STAVE, + MANUAL_AUTO_CAST, + MELEE_OR_RANGE +} diff --git a/src/main/java/com/attacktimer/AttackType.java b/src/main/java/com/attacktimer/AttackType.java index 0608da7..cbc45e2 100644 --- a/src/main/java/com/attacktimer/AttackType.java +++ b/src/main/java/com/attacktimer/AttackType.java @@ -1,42 +1,42 @@ -package com.attacktimer; - -/* - * 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. - */ - -public enum AttackType -{ - CRUSH, - SLASH, - STAB, - RANGED, - MAGIC, - NONE, - ; - - public boolean IsMelee() - { - return this.equals(CRUSH) || this.equals(SLASH) || this.equals(STAB); - } +package com.attacktimer; + +/* + * 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. + */ + +public enum AttackType +{ + CRUSH, + SLASH, + STAB, + RANGED, + MAGIC, + NONE, + ; + + public boolean IsMelee() + { + return this.equals(CRUSH) || this.equals(SLASH) || this.equals(STAB); + } } \ No newline at end of file diff --git a/src/main/java/com/attacktimer/ClientUtils/Utils.java b/src/main/java/com/attacktimer/ClientUtils/Utils.java index 15114a3..e7c0dfe 100644 --- a/src/main/java/com/attacktimer/ClientUtils/Utils.java +++ b/src/main/java/com/attacktimer/ClientUtils/Utils.java @@ -1,177 +1,177 @@ -package com.attacktimer.ClientUtils; - -/* - * Copyright (c) 2022, Nick Graves - * Copyright (c) 2024-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.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.Item; -import net.runelite.api.ItemContainer; -import net.runelite.api.NPC; -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 -{ - public static int getItemIdFromContainer(ItemContainer container, int slotID) - { - if (container == null) - { - return -1; - } - final Item item = container.getItem(slotID); - return (item != null) ? item.getId() : -1; - } - - public static int getWeaponId(Client client) - { - return getItemIdFromContainer(client.getItemContainer(InventoryID.WORN), - EquipmentInventorySlot.WEAPON.getSlotIdx()); - } - - // getLocation will return the current world point of the player accounting for instances. - // - // For computing tile based distances you probably don't want this and instead should use - // client.getLocalPlayer().getWorldLocation(). - public static WorldPoint getLocalLocation(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) - { - final AttackStyle[] attackStyles = getWeaponType(client).getAttackStyles(client); - 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]; - } - - return AttackStyle.ACCURATE; - } - - // returns null for unknown weapons - public static WeaponType getWeaponType(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) - { - final WeaponType weaponType = getWeaponType(client); - final int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE); - if (currentAttackStyleVarbit < weaponType.getAttackTypes().length) - { - return weaponType.getAttackTypes()[currentAttackStyleVarbit]; - } - return null; - } - - // returns zero for no target - public static int getTargetId(Client client) - { - final NPC target = getTargetNPC(client); - int targetId = 0; - if (target != null) - { - targetId = target.getId(); - } - return targetId; - } - - // returns null for no target - public static NPC getTargetNPC(Client client) - { - final Actor target = client.getLocalPlayer().getInteracting(); - if (target != null && (target instanceof NPC)) - { - final NPC npc = (NPC) target; - return npc; - } - 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; - } -} +package com.attacktimer.ClientUtils; + +/* + * Copyright (c) 2022, Nick Graves + * Copyright (c) 2024-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.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.Item; +import net.runelite.api.ItemContainer; +import net.runelite.api.NPC; +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 +{ + public static int getItemIdFromContainer(ItemContainer container, int slotID) + { + if (container == null) + { + return -1; + } + final Item item = container.getItem(slotID); + return (item != null) ? item.getId() : -1; + } + + public static int getWeaponId(Client client) + { + return getItemIdFromContainer(client.getItemContainer(InventoryID.WORN), + EquipmentInventorySlot.WEAPON.getSlotIdx()); + } + + // getLocation will return the current world point of the player accounting for instances. + // + // For computing tile based distances you probably don't want this and instead should use + // client.getLocalPlayer().getWorldLocation(). + public static WorldPoint getLocalLocation(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) + { + final AttackStyle[] attackStyles = getWeaponType(client).getAttackStyles(client); + 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]; + } + + return AttackStyle.ACCURATE; + } + + // returns null for unknown weapons + public static WeaponType getWeaponType(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) + { + final WeaponType weaponType = getWeaponType(client); + final int currentAttackStyleVarbit = client.getVarpValue(VarPlayerID.COM_MODE); + if (currentAttackStyleVarbit < weaponType.getAttackTypes().length) + { + return weaponType.getAttackTypes()[currentAttackStyleVarbit]; + } + return null; + } + + // returns zero for no target + public static int getTargetId(Client client) + { + final NPC target = getTargetNPC(client); + int targetId = 0; + if (target != null) + { + targetId = target.getId(); + } + return targetId; + } + + // returns null for no target + public static NPC getTargetNPC(Client client) + { + final Actor target = client.getLocalPlayer().getInteracting(); + if (target != null && (target instanceof NPC)) + { + final NPC npc = (NPC) target; + return npc; + } + 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/Damage.java b/src/main/java/com/attacktimer/Damage.java index 7a1054f..6999f55 100644 --- a/src/main/java/com/attacktimer/Damage.java +++ b/src/main/java/com/attacktimer/Damage.java @@ -1,111 +1,111 @@ -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 java.util.ArrayDeque; -import net.runelite.api.Skill; -import net.runelite.api.events.FakeXpDrop; -import net.runelite.api.events.StatChanged; - -/** - * Damage is a helper store that from the HP exp alone can compute the predicted damage of an attack. - * - * It works by storing the queue of hp exp drops then computing the delta of those drops then the normal - * damage formula (hpExp * 3/4 == damage). - * - * TODO npc exp modifiers - * - * TODO global modifiers - */ -public class Damage -{ - private static final double MODIFIER = 1; - private static final double GLOBAL_MODIFIER = 1; - - private ArrayDeque hpExpEarned = new ArrayDeque(); - private ArrayDeque hpExpEarnedTickCount = new ArrayDeque(); - - public boolean onXpDrop(StatChanged event, TickCount tc) - { - if (event.getSkill() != Skill.HITPOINTS) - { - return false; - } - hpExpEarnedTickCount.addLast(tc.get()); - hpExpEarned.addLast(event.getXp()); - return true; - } - - public boolean onXpDrop(FakeXpDrop event, TickCount tc) - { - if (event.getSkill() != Skill.HITPOINTS) - { - return false; - } - hpExpEarnedTickCount.addLast(tc.get()); - hpExpEarnedTickCount.addLast(tc.get()); - // Fake exp doesn't have a delta like real xp - hpExpEarned.addLast(0); - hpExpEarned.addLast(event.getXp()); - return true; - } - - /** - * compute determines from the previous hp exp drops how much damage the player has dealt on this tick. - * @param tc the tick count state - * @return the amount of damage dealt this tick - */ - public int compute(TickCount tc) - { - if (hpExpEarnedTickCount.isEmpty()) - { - return -1; - } - final var lastTc = hpExpEarnedTickCount.getLast(); - if (!tc.isWithinNTicks(lastTc, 1)) - { - // In this case the last exp tick wasn't this tick in which case we hit a 0. - return 0; - } - // https://oldschool.runescape.wiki/w/Combat#Experience_gain - final var xp = (double) Utils.getLastDelta(hpExpEarned); - return (int) Math.round(xp * (3.0d / 4.0d) * MODIFIER * GLOBAL_MODIFIER); - } - - public void cleanup() - { - while (hpExpEarnedTickCount.size() > 5) - { - hpExpEarnedTickCount.removeFirst(); - } - while (hpExpEarned.size() > 5) - { - hpExpEarned.removeFirst(); - } - } -} +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 java.util.ArrayDeque; +import net.runelite.api.Skill; +import net.runelite.api.events.FakeXpDrop; +import net.runelite.api.events.StatChanged; + +/** + * Damage is a helper store that from the HP exp alone can compute the predicted damage of an attack. + * + * It works by storing the queue of hp exp drops then computing the delta of those drops then the normal + * damage formula (hpExp * 3/4 == damage). + * + * TODO npc exp modifiers + * + * TODO global modifiers + */ +public class Damage +{ + private static final double MODIFIER = 1; + private static final double GLOBAL_MODIFIER = 1; + + private ArrayDeque hpExpEarned = new ArrayDeque(); + private ArrayDeque hpExpEarnedTickCount = new ArrayDeque(); + + public boolean onXpDrop(StatChanged event, TickCount tc) + { + if (event.getSkill() != Skill.HITPOINTS) + { + return false; + } + hpExpEarnedTickCount.addLast(tc.get()); + hpExpEarned.addLast(event.getXp()); + return true; + } + + public boolean onXpDrop(FakeXpDrop event, TickCount tc) + { + if (event.getSkill() != Skill.HITPOINTS) + { + return false; + } + hpExpEarnedTickCount.addLast(tc.get()); + hpExpEarnedTickCount.addLast(tc.get()); + // Fake exp doesn't have a delta like real xp + hpExpEarned.addLast(0); + hpExpEarned.addLast(event.getXp()); + return true; + } + + /** + * compute determines from the previous hp exp drops how much damage the player has dealt on this tick. + * @param tc the tick count state + * @return the amount of damage dealt this tick + */ + public int compute(TickCount tc) + { + if (hpExpEarnedTickCount.isEmpty()) + { + return -1; + } + final var lastTc = hpExpEarnedTickCount.getLast(); + if (!tc.isWithinNTicks(lastTc, 1)) + { + // In this case the last exp tick wasn't this tick in which case we hit a 0. + return 0; + } + // https://oldschool.runescape.wiki/w/Combat#Experience_gain + final var xp = (double) Utils.getLastDelta(hpExpEarned); + return (int) Math.round(xp * (3.0d / 4.0d) * MODIFIER * GLOBAL_MODIFIER); + } + + public void cleanup() + { + while (hpExpEarnedTickCount.size() > 5) + { + hpExpEarnedTickCount.removeFirst(); + } + while (hpExpEarned.size() > 5) + { + hpExpEarned.removeFirst(); + } + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/Amoxliatl.java b/src/main/java/com/attacktimer/VariableSpeed/Amoxliatl.java index 3d720f9..0d5ebaa 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/Amoxliatl.java +++ b/src/main/java/com/attacktimer/VariableSpeed/Amoxliatl.java @@ -1,60 +1,60 @@ -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.AttackType; -import com.attacktimer.ClientUtils.Utils; -import com.attacktimer.Spellbook; -import net.runelite.api.Client; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.NpcID; - -/** - * Amoxliatl: https://oldschool.runescape.wiki/w/Unstable_ice - * - * The unstable ice blocks can only be destroyed with melee, and melee attacks against them will result in a - * max hit with an attack speed of 1. - */ -public class Amoxliatl implements IVariableSpeed -{ - private static final int AMOXLIATL_REGION_ID = 5446; - - 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 WorldPoint location = Utils.getLocalLocation(client); - final int targetId = Utils.getTargetId(client); - final AttackType attkType = Utils.getAttackType(client); - if (location.getRegionID() == AMOXLIATL_REGION_ID && targetId == NpcID.AMOXLIATL_ICE_BLOCK && attkType.IsMelee()) - { - 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.AttackType; +import com.attacktimer.ClientUtils.Utils; +import com.attacktimer.Spellbook; +import net.runelite.api.Client; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.NpcID; + +/** + * Amoxliatl: https://oldschool.runescape.wiki/w/Unstable_ice + * + * The unstable ice blocks can only be destroyed with melee, and melee attacks against them will result in a + * max hit with an attack speed of 1. + */ +public class Amoxliatl implements IVariableSpeed +{ + private static final int AMOXLIATL_REGION_ID = 5446; + + 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 WorldPoint location = Utils.getLocalLocation(client); + final int targetId = Utils.getTargetId(client); + final AttackType attkType = Utils.getAttackType(client); + if (location.getRegionID() == AMOXLIATL_REGION_ID && targetId == NpcID.AMOXLIATL_ICE_BLOCK && attkType.IsMelee()) + { + return 1; + } + return curSpeed; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/BloodMoonSet.java b/src/main/java/com/attacktimer/VariableSpeed/BloodMoonSet.java index 4558512..91d43c2 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/BloodMoonSet.java +++ b/src/main/java/com/attacktimer/VariableSpeed/BloodMoonSet.java @@ -1,46 +1,46 @@ -package com.attacktimer.VariableSpeed; - -/* - * 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.Spellbook; -import net.runelite.api.Client; -import net.runelite.api.gameval.SpotanimID; - -public class BloodMoonSet implements IVariableSpeed -{ - 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) - { - if (client.getLocalPlayer().hasSpotAnim(SpotanimID.SPECIAL_DUAL_MACUAHUITL_SPOTANIM)) - { - return curSpeed - 1; - } - return curSpeed; - } -} +package com.attacktimer.VariableSpeed; + +/* + * 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.Spellbook; +import net.runelite.api.Client; +import net.runelite.api.gameval.SpotanimID; + +public class BloodMoonSet implements IVariableSpeed +{ + 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) + { + if (client.getLocalPlayer().hasSpotAnim(SpotanimID.SPECIAL_DUAL_MACUAHUITL_SPOTANIM)) + { + return curSpeed - 1; + } + return curSpeed; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/EyeOfAyak.java b/src/main/java/com/attacktimer/VariableSpeed/EyeOfAyak.java index 93317ea..13ee161 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/EyeOfAyak.java +++ b/src/main/java/com/attacktimer/VariableSpeed/EyeOfAyak.java @@ -1,51 +1,51 @@ -package com.attacktimer.VariableSpeed; - -/* - * Copyright (c) 2025, 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.Spellbook; -import net.runelite.api.Client; - -public class EyeOfAyak implements IVariableSpeed -{ - 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) - { - // https://oldschool.runescape.wiki/w/Eye_of_ayak#Charged - // https://oldschool.runescape.wiki/w/Eye_of_ayak#Special_attack - if (curAnimation == AnimationData.MAGIC_EYE_OF_AYAK_SPEC) - { - // This is unclear if the Ayak spec "sets" the speed to 5 or adds two ticks. We can't know until - // leagues. If it does set it too 5 this code is correct and it's order in - // `src\main\java\com\attacktimer\VariableSpeed\VariableSpeed.java` is correct. However if it's - // actually a +2 this code is wrong and it *might* need to come before the leagues modifier. - return 5; - } - return curSpeed; - } -} +package com.attacktimer.VariableSpeed; + +/* + * Copyright (c) 2025, 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.Spellbook; +import net.runelite.api.Client; + +public class EyeOfAyak implements IVariableSpeed +{ + 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) + { + // https://oldschool.runescape.wiki/w/Eye_of_ayak#Charged + // https://oldschool.runescape.wiki/w/Eye_of_ayak#Special_attack + if (curAnimation == AnimationData.MAGIC_EYE_OF_AYAK_SPEC) + { + // This is unclear if the Ayak spec "sets" the speed to 5 or adds two ticks. We can't know until + // leagues. If it does set it too 5 this code is correct and it's order in + // `src\main\java\com\attacktimer\VariableSpeed\VariableSpeed.java` is correct. However if it's + // actually a +2 this code is wrong and it *might* need to come before the leagues modifier. + return 5; + } + return curSpeed; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/IVariableSpeed.java b/src/main/java/com/attacktimer/VariableSpeed/IVariableSpeed.java index 276ab87..1a84846 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/IVariableSpeed.java +++ b/src/main/java/com/attacktimer/VariableSpeed/IVariableSpeed.java @@ -1,66 +1,66 @@ -package com.attacktimer.VariableSpeed; - -/* - * Copyright (c) 2024-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.Spellbook; -import com.attacktimer.VariableSpeed.State.IStateTracker; -import net.runelite.api.Client; - -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. 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 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( - final Client client, - final AnimationData curAnimation, - final AttackProcedure atkType, - final Spellbook spellbook, - final int damageDealt, - final int lastSpecDelta, - final int baseSpeed, - final int curSpeed - ); +package com.attacktimer.VariableSpeed; + +/* + * Copyright (c) 2024-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.Spellbook; +import com.attacktimer.VariableSpeed.State.IStateTracker; +import net.runelite.api.Client; + +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. 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 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( + final Client client, + final AnimationData curAnimation, + final AttackProcedure atkType, + final Spellbook spellbook, + 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/PurgingStaffSpec.java b/src/main/java/com/attacktimer/VariableSpeed/PurgingStaffSpec.java index 3683850..172c987 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/PurgingStaffSpec.java +++ b/src/main/java/com/attacktimer/VariableSpeed/PurgingStaffSpec.java @@ -1,82 +1,82 @@ -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.Spellbook; -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 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 Spellbook spellbook, 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; - } - final var target = Utils.getTargetNPC(client); - final var flare = Yama.isEitherVoidFlare(target, lastTarget); - lastTarget = target; - if (flare == null || yama == null) - { - return curSpeed; - } - - yama.dealVoidFlareDamage(flare, damageDealt); - if (lastSpecDelta != -250 || Utils.getWeaponId(client) != ItemID.PURGING_STAFF || spellbook != Spellbook.ARCEUUS) - { - // not using the spec - // not using a purging staff - // not on the arceuss spellbook - 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; - } -} +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.Spellbook; +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 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 Spellbook spellbook, 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; + } + final var target = Utils.getTargetNPC(client); + final var flare = Yama.isEitherVoidFlare(target, lastTarget); + lastTarget = target; + if (flare == null || yama == null) + { + return curSpeed; + } + + yama.dealVoidFlareDamage(flare, damageDealt); + if (lastSpecDelta != -250 || Utils.getWeaponId(client) != ItemID.PURGING_STAFF || spellbook != Spellbook.ARCEUUS) + { + // not using the spec + // not using a purging staff + // not on the arceuss spellbook + 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 2e9aafd..db63b9b 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/RapidAttackStyle.java +++ b/src/main/java/com/attacktimer/VariableSpeed/RapidAttackStyle.java @@ -1,52 +1,52 @@ -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 com.attacktimer.Spellbook; -import net.runelite.api.Client; -import net.runelite.api.gameval.VarPlayerID; - -public class RapidAttackStyle implements IVariableSpeed -{ - 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) - { - // index 1 == rapid - final boolean isRapid = client.getVarpValue(VarPlayerID.COM_MODE) == 1; - 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; - } -} +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 com.attacktimer.Spellbook; +import net.runelite.api.Client; +import net.runelite.api.gameval.VarPlayerID; + +public class RapidAttackStyle implements IVariableSpeed +{ + 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) + { + // index 1 == rapid + final boolean isRapid = client.getVarpValue(VarPlayerID.COM_MODE) == 1; + 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; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/RedKerisSpec.java b/src/main/java/com/attacktimer/VariableSpeed/RedKerisSpec.java index 7dbbb8b..b5bf541 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/RedKerisSpec.java +++ b/src/main/java/com/attacktimer/VariableSpeed/RedKerisSpec.java @@ -1,51 +1,51 @@ -package com.attacktimer.VariableSpeed; - -/* - * 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.Spellbook; -import net.runelite.api.Client; - -public class RedKerisSpec implements IVariableSpeed -{ - 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) - { - // https://oldschool.runescape.wiki/w/Keris_partisan_of_corruption#Special_attack - if (lastSpecDelta != -750 || curAnimation != AnimationData.MELEE_RED_KERIS_SPEC) - { - return curSpeed; - } - // We are using the special attack, but we only slow down if we hit a number - if (damageDealt > 0) - { - return curSpeed + 4; - } - return curSpeed; - } -} +package com.attacktimer.VariableSpeed; + +/* + * 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.Spellbook; +import net.runelite.api.Client; + +public class RedKerisSpec implements IVariableSpeed +{ + 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) + { + // https://oldschool.runescape.wiki/w/Keris_partisan_of_corruption#Special_attack + if (lastSpecDelta != -750 || curAnimation != AnimationData.MELEE_RED_KERIS_SPEC) + { + return curSpeed; + } + // We are using the special attack, but we only slow down if we hit a number + if (damageDealt > 0) + { + return curSpeed + 4; + } + return curSpeed; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/RoyalTitans.java b/src/main/java/com/attacktimer/VariableSpeed/RoyalTitans.java index 4f5a0ba..5da4c04 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/RoyalTitans.java +++ b/src/main/java/com/attacktimer/VariableSpeed/RoyalTitans.java @@ -1,263 +1,263 @@ -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.Spellbook; -import com.google.common.collect.ImmutableSet; -import java.util.HashSet; -import java.util.Set; -import net.runelite.api.Client; -import net.runelite.api.NPC; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; -import net.runelite.api.gameval.ItemID; -import net.runelite.api.gameval.NpcID; - -/** - * RoyalTitans: https://oldschool.runescape.wiki/w/Royal_Titans/Strategies - * - * For each elemental killed, the player receives a 1-tick reduction to their attack delay, allowing spells to - * be cast consecutively much quicker than usual. - */ -public class RoyalTitans implements IVariableSpeed -{ - private static final int ROYAL_TITANS_REGION_ID = 11669; - - private static final int HP_FUDGE = 1; - private static final int ELEMENTAL_HP = 40 - HP_FUDGE; - - private Set iceElementals = new HashSet(); - private Set fireElementals = new HashSet(); - - private boolean removeDead = false; - - private static final Set ONE_SHOT_SPELLS = new ImmutableSet.Builder() - .add(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST) - .add(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF) - .add(AnimationData.MAGIC_STANDARD_SURGE_STAFF) - .add(AnimationData.MAGIC_STANDARD_WAVE) - .add(AnimationData.MAGIC_STANDARD_WAVE_STAFF) - .build(); - - @Override - 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) - { - if (earlyExit(client, atkType, damageDealt, spellbook)) - { - return curSpeed; - } - final int targetId = Utils.getTargetId(client); - if (spellbook != Spellbook.STANDARD - || !isElemental(targetId) - || !ONE_SHOT_SPELLS.contains(curAnimation)) - { - return curSpeed; - } - final NPC target = Utils.getTargetNPC(client); - if (target == null) - { - return curSpeed; - } - // We are now in the royal titan region, attacking an elemental using magic and one of the standard - // spells which could one shot it. - - // Note that the twinflame second spell does not give Magic or Hitpoints experience and therefore will - // not be computed properly by the caller. - final boolean wieldingTwinflame = Utils.getWeaponId(client) == ItemID.TWINFLAME_STAFF; - final int computedDamage = wieldingTwinflame ? damageDealt + ((damageDealt * 4) / 10) : damageDealt; - - // Awkwardly you only got awarded the exp (and therefore computed damage) against the one of the - // elementals, hence the 3x3 AoE isn't seen in the damage dealt. - if (computedDamage < ELEMENTAL_HP) - { - // Don't bother computing partial damage assume most players are one-shotting - return curSpeed; - } - // Compute the number of elementals in the a 3x3 from our target: - final var set = targetId == NpcID.RT_SUMMON_ELEMENTAL_FIRE ? fireElementals : iceElementals; - int count = 1; - final var reference = target.getWorldLocation(); - for (final NPC elemental : set) - { - if (elemental == target) - { - continue; - } - final WorldPoint worldLocation = elemental.getWorldLocation(); - final int distanceTo2D = reference.distanceTo2D(worldLocation); - // 3x3 is 1 distance https://en.wikipedia.org/wiki/Chebyshev_distance - if (distanceTo2D <= 1) - { - count++; - } - } - // Now compute the travel delay, we are only awarded the improved tick delay when the projectile lands - // (this can be pre-computed) so if we kill 3 elementals 10 tiles away we don't see the full 3 tick - // improvement but in fact we see the 3 ticks awarded 4 ticks after we attacked. And because of - // https://oldschool.runescape.wiki/w/Hit_delay#Processing_order_delay: - // - // > NPCs are processed earlier than players each tick, so this effect will make all hits on NPCs - // > delayed by an additional one tick compared to the numbers listed in this article. - // - // This means the 3 tick reduction is awarded on tick 5, by which time we're already off-cool down - // (assuming manual cast). And if we use the twin-flame (6 tick) we will only get a single tick of the - // improvement we earned. - // - // Therefore this is generalised as: - // - // We calculate the tick on which the reductions take effect (hitDelay + 1). A reduction of N ticks - // only benefits us if it lands **before** the natural attack timer expires. - // - // Effective ready tick = max(hitDelay + 1, curSpeed - elementals_killed) - - // NOTE: This is probably why the purging staff has that bug, because it only awards the 3 ticks of - // reduction when the spell lands (which is always 2 ticks for dark demon bane). - - // NOTE: this has one edge case also not covered (as well as the same eating one as purging staff) - // which is that if the awarded bonus is granted and we already started our next weapon cooldown then - // the bonus is applied to that attack instead. - - final boolean isTwinflameKillOnSecondProjectile = wieldingTwinflame && - curAnimation == AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF; - final int extraHitOffset = isTwinflameKillOnSecondProjectile ? 1 : 0; // add an extra tick of delay for the second projectile. - - // https://oldschool.runescape.wiki/w/Hit_delay#Magic However it's not quite the formula, unclear to - // me whether the wiki is wrong or if its simply a case of fence post errors counting the tiles. - // https://en.wikipedia.org/wiki/Off-by-one_error#Fencepost_error - // - // from my testing the hit delay was 1 less on the transitions in the table therefore: - // - // Distance Hit delay - // (tiles) (wiki) (measured) - // 1 1 1 - // 2 2 1 (diff) - // 3 2 2 - // 4 2 2 - // 5 3 2 (diff) - // 6 3 3 - // 7 3 3 - // 8 4 3 (diff) - // 9 4 4 - // 10 4 4 - final WorldPoint playerLoc = client.getLocalPlayer().getWorldLocation(); - final int distance = playerLoc.distanceTo2D(reference); - final int hitDelay = 2 + (distance / 3) + extraHitOffset; - - // Remaining cooldown when the projectile actually impacts: - final int remainingAtImpact = Math.max(0, curSpeed - hitDelay); - - // Apply reduction to remaining cooldown: - final int remainingAfterReduction = Math.max(0, remainingAtImpact - count); - - // Total ticks waited = travel time + remaining cooldown after reduction - final int finalSpeed = Math.min(curSpeed, hitDelay + remainingAfterReduction); - - // despawn happens much later than is dead (hence how Entity Hider works) so we need to remove them - // now if we succeeded in apply. Deferred till the next onGameTick is called. - removeDead = true; - return finalSpeed; - } - - private static boolean earlyExit(final Client client, final AttackProcedure atkType, final int damageDealt, - final Spellbook spellbook) - { - return notInRegion(client) - || atkType != AttackProcedure.MANUAL_AUTO_CAST - || damageDealt <= 0 - || spellbook != Spellbook.STANDARD; - } - - @Override - public void onGameTick(Client client, GameTick tick) - { - // if we killed some elementals last tick remove them now. - if (removeDead) - { - // don't clear the set(s) do it using the runelite API. - var removed = iceElementals.removeIf(npc -> npc.isDead()); - removed |= fireElementals.removeIf(npc -> npc.isDead()); - if (removed) - { - removeDead = false; - } - } - } - - @Override - public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) - { - if (notInRegion(client)) - { - return; - } - final NPC npc = npcSpawned.getNpc(); - final int id = npc.getId(); - if (id == NpcID.RT_SUMMON_ELEMENTAL_ICE) - { - iceElementals.add(npc); - } - else if (id == NpcID.RT_SUMMON_ELEMENTAL_FIRE) - { - fireElementals.add(npc); - } - } - - @Override - public void onNpcDespawned(final Client client, final NpcDespawned npcDespawned) - { - if (notInRegion(client)) - { - return; - } - final NPC npc = npcDespawned.getNpc(); - final int id = npc.getId(); - if (id == NpcID.RT_SUMMON_ELEMENTAL_ICE) - { - iceElementals.remove(npc); - } - else if (id == NpcID.RT_SUMMON_ELEMENTAL_FIRE) - { - fireElementals.remove(npc); - } - } - - private static boolean isElemental(int id) - { - return id == NpcID.RT_SUMMON_ELEMENTAL_FIRE || id == NpcID.RT_SUMMON_ELEMENTAL_ICE; - } - - private static boolean notInRegion(final Client client) - { - return Utils.getLocalLocation(client).getRegionID() != ROYAL_TITANS_REGION_ID; - } -} +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.Spellbook; +import com.google.common.collect.ImmutableSet; +import java.util.HashSet; +import java.util.Set; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +import net.runelite.api.gameval.ItemID; +import net.runelite.api.gameval.NpcID; + +/** + * RoyalTitans: https://oldschool.runescape.wiki/w/Royal_Titans/Strategies + * + * For each elemental killed, the player receives a 1-tick reduction to their attack delay, allowing spells to + * be cast consecutively much quicker than usual. + */ +public class RoyalTitans implements IVariableSpeed +{ + private static final int ROYAL_TITANS_REGION_ID = 11669; + + private static final int HP_FUDGE = 1; + private static final int ELEMENTAL_HP = 40 - HP_FUDGE; + + private Set iceElementals = new HashSet(); + private Set fireElementals = new HashSet(); + + private boolean removeDead = false; + + private static final Set ONE_SHOT_SPELLS = new ImmutableSet.Builder() + .add(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST) + .add(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF) + .add(AnimationData.MAGIC_STANDARD_SURGE_STAFF) + .add(AnimationData.MAGIC_STANDARD_WAVE) + .add(AnimationData.MAGIC_STANDARD_WAVE_STAFF) + .build(); + + @Override + 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) + { + if (earlyExit(client, atkType, damageDealt, spellbook)) + { + return curSpeed; + } + final int targetId = Utils.getTargetId(client); + if (spellbook != Spellbook.STANDARD + || !isElemental(targetId) + || !ONE_SHOT_SPELLS.contains(curAnimation)) + { + return curSpeed; + } + final NPC target = Utils.getTargetNPC(client); + if (target == null) + { + return curSpeed; + } + // We are now in the royal titan region, attacking an elemental using magic and one of the standard + // spells which could one shot it. + + // Note that the twinflame second spell does not give Magic or Hitpoints experience and therefore will + // not be computed properly by the caller. + final boolean wieldingTwinflame = Utils.getWeaponId(client) == ItemID.TWINFLAME_STAFF; + final int computedDamage = wieldingTwinflame ? damageDealt + ((damageDealt * 4) / 10) : damageDealt; + + // Awkwardly you only got awarded the exp (and therefore computed damage) against the one of the + // elementals, hence the 3x3 AoE isn't seen in the damage dealt. + if (computedDamage < ELEMENTAL_HP) + { + // Don't bother computing partial damage assume most players are one-shotting + return curSpeed; + } + // Compute the number of elementals in the a 3x3 from our target: + final var set = targetId == NpcID.RT_SUMMON_ELEMENTAL_FIRE ? fireElementals : iceElementals; + int count = 1; + final var reference = target.getWorldLocation(); + for (final NPC elemental : set) + { + if (elemental == target) + { + continue; + } + final WorldPoint worldLocation = elemental.getWorldLocation(); + final int distanceTo2D = reference.distanceTo2D(worldLocation); + // 3x3 is 1 distance https://en.wikipedia.org/wiki/Chebyshev_distance + if (distanceTo2D <= 1) + { + count++; + } + } + // Now compute the travel delay, we are only awarded the improved tick delay when the projectile lands + // (this can be pre-computed) so if we kill 3 elementals 10 tiles away we don't see the full 3 tick + // improvement but in fact we see the 3 ticks awarded 4 ticks after we attacked. And because of + // https://oldschool.runescape.wiki/w/Hit_delay#Processing_order_delay: + // + // > NPCs are processed earlier than players each tick, so this effect will make all hits on NPCs + // > delayed by an additional one tick compared to the numbers listed in this article. + // + // This means the 3 tick reduction is awarded on tick 5, by which time we're already off-cool down + // (assuming manual cast). And if we use the twin-flame (6 tick) we will only get a single tick of the + // improvement we earned. + // + // Therefore this is generalised as: + // + // We calculate the tick on which the reductions take effect (hitDelay + 1). A reduction of N ticks + // only benefits us if it lands **before** the natural attack timer expires. + // + // Effective ready tick = max(hitDelay + 1, curSpeed - elementals_killed) + + // NOTE: This is probably why the purging staff has that bug, because it only awards the 3 ticks of + // reduction when the spell lands (which is always 2 ticks for dark demon bane). + + // NOTE: this has one edge case also not covered (as well as the same eating one as purging staff) + // which is that if the awarded bonus is granted and we already started our next weapon cooldown then + // the bonus is applied to that attack instead. + + final boolean isTwinflameKillOnSecondProjectile = wieldingTwinflame && + curAnimation == AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF; + final int extraHitOffset = isTwinflameKillOnSecondProjectile ? 1 : 0; // add an extra tick of delay for the second projectile. + + // https://oldschool.runescape.wiki/w/Hit_delay#Magic However it's not quite the formula, unclear to + // me whether the wiki is wrong or if its simply a case of fence post errors counting the tiles. + // https://en.wikipedia.org/wiki/Off-by-one_error#Fencepost_error + // + // from my testing the hit delay was 1 less on the transitions in the table therefore: + // + // Distance Hit delay + // (tiles) (wiki) (measured) + // 1 1 1 + // 2 2 1 (diff) + // 3 2 2 + // 4 2 2 + // 5 3 2 (diff) + // 6 3 3 + // 7 3 3 + // 8 4 3 (diff) + // 9 4 4 + // 10 4 4 + final WorldPoint playerLoc = client.getLocalPlayer().getWorldLocation(); + final int distance = playerLoc.distanceTo2D(reference); + final int hitDelay = 2 + (distance / 3) + extraHitOffset; + + // Remaining cooldown when the projectile actually impacts: + final int remainingAtImpact = Math.max(0, curSpeed - hitDelay); + + // Apply reduction to remaining cooldown: + final int remainingAfterReduction = Math.max(0, remainingAtImpact - count); + + // Total ticks waited = travel time + remaining cooldown after reduction + final int finalSpeed = Math.min(curSpeed, hitDelay + remainingAfterReduction); + + // despawn happens much later than is dead (hence how Entity Hider works) so we need to remove them + // now if we succeeded in apply. Deferred till the next onGameTick is called. + removeDead = true; + return finalSpeed; + } + + private static boolean earlyExit(final Client client, final AttackProcedure atkType, final int damageDealt, + final Spellbook spellbook) + { + return notInRegion(client) + || atkType != AttackProcedure.MANUAL_AUTO_CAST + || damageDealt <= 0 + || spellbook != Spellbook.STANDARD; + } + + @Override + public void onGameTick(Client client, GameTick tick) + { + // if we killed some elementals last tick remove them now. + if (removeDead) + { + // don't clear the set(s) do it using the runelite API. + var removed = iceElementals.removeIf(npc -> npc.isDead()); + removed |= fireElementals.removeIf(npc -> npc.isDead()); + if (removed) + { + removeDead = false; + } + } + } + + @Override + public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) + { + if (notInRegion(client)) + { + return; + } + final NPC npc = npcSpawned.getNpc(); + final int id = npc.getId(); + if (id == NpcID.RT_SUMMON_ELEMENTAL_ICE) + { + iceElementals.add(npc); + } + else if (id == NpcID.RT_SUMMON_ELEMENTAL_FIRE) + { + fireElementals.add(npc); + } + } + + @Override + public void onNpcDespawned(final Client client, final NpcDespawned npcDespawned) + { + if (notInRegion(client)) + { + return; + } + final NPC npc = npcDespawned.getNpc(); + final int id = npc.getId(); + if (id == NpcID.RT_SUMMON_ELEMENTAL_ICE) + { + iceElementals.remove(npc); + } + else if (id == NpcID.RT_SUMMON_ELEMENTAL_FIRE) + { + fireElementals.remove(npc); + } + } + + private static boolean isElemental(int id) + { + return id == NpcID.RT_SUMMON_ELEMENTAL_FIRE || id == NpcID.RT_SUMMON_ELEMENTAL_ICE; + } + + private static boolean notInRegion(final Client client) + { + return Utils.getLocalLocation(client).getRegionID() != ROYAL_TITANS_REGION_ID; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/Scurrius.java b/src/main/java/com/attacktimer/VariableSpeed/Scurrius.java index eb1b163..3b6319e 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/Scurrius.java +++ b/src/main/java/com/attacktimer/VariableSpeed/Scurrius.java @@ -1,76 +1,76 @@ -package com.attacktimer.VariableSpeed; - -/* - * Copyright (c) 2024-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.Spellbook; -import net.runelite.api.Client; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.gameval.ItemID; -import net.runelite.api.gameval.NpcID; - -/** - * Scurrius: https://oldschool.runescape.wiki/w/Scurrius/Strategies#Strategies - * - * When attacking the giant rats summoned by Scurrius and attacking them with a bone weapon the player has no - * attack delay. - */ -public class Scurrius implements IVariableSpeed -{ - private static final int SCURRIUS_REGION_ID = 13210; - - private static final int SCURRIUS_MIN_X = 3290; - private static final int SCURRIUS_MAX_X = 3307; - - private static final int SCURRIUS_MIN_Y = 9859; - private static final int SCURRIUS_MAX_Y = 9876; - - private static boolean attackingGiantRatWithBoneWeapon(final int equipped, final int regionId, final int x, - final int y, final int target) - { - final boolean correctWeapon = equipped == ItemID.RAT_BONE_STAFF || equipped == ItemID.RAT_BONE_MACE || equipped == ItemID.RAT_BONE_BOW; - final boolean correctCoords = x >= SCURRIUS_MIN_X && x <= SCURRIUS_MAX_X && y >= SCURRIUS_MIN_Y && y <= SCURRIUS_MAX_Y; - final boolean correctRegion = regionId == SCURRIUS_REGION_ID; - final boolean correctEnemy = target == NpcID.RAT_BOSS_GIANT_RAT; - return correctWeapon && correctCoords && correctRegion && correctEnemy; - } - - 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 WorldPoint location = Utils.getLocalLocation(client); - final int weaponId = Utils.getWeaponId(client); - final int targetId = Utils.getTargetId(client); - if (attackingGiantRatWithBoneWeapon(weaponId, location.getRegionID(), location.getX(), location.getY(), targetId)) - { - return 1; - } - return curSpeed; - } -} +package com.attacktimer.VariableSpeed; + +/* + * Copyright (c) 2024-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.Spellbook; +import net.runelite.api.Client; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.ItemID; +import net.runelite.api.gameval.NpcID; + +/** + * Scurrius: https://oldschool.runescape.wiki/w/Scurrius/Strategies#Strategies + * + * When attacking the giant rats summoned by Scurrius and attacking them with a bone weapon the player has no + * attack delay. + */ +public class Scurrius implements IVariableSpeed +{ + private static final int SCURRIUS_REGION_ID = 13210; + + private static final int SCURRIUS_MIN_X = 3290; + private static final int SCURRIUS_MAX_X = 3307; + + private static final int SCURRIUS_MIN_Y = 9859; + private static final int SCURRIUS_MAX_Y = 9876; + + private static boolean attackingGiantRatWithBoneWeapon(final int equipped, final int regionId, final int x, + final int y, final int target) + { + final boolean correctWeapon = equipped == ItemID.RAT_BONE_STAFF || equipped == ItemID.RAT_BONE_MACE || equipped == ItemID.RAT_BONE_BOW; + final boolean correctCoords = x >= SCURRIUS_MIN_X && x <= SCURRIUS_MAX_X && y >= SCURRIUS_MIN_Y && y <= SCURRIUS_MAX_Y; + final boolean correctRegion = regionId == SCURRIUS_REGION_ID; + final boolean correctEnemy = target == NpcID.RAT_BOSS_GIANT_RAT; + return correctWeapon && correctCoords && correctRegion && correctEnemy; + } + + 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 WorldPoint location = Utils.getLocalLocation(client); + final int weaponId = Utils.getWeaponId(client); + final int targetId = Utils.getTargetId(client); + if (attackingGiantRatWithBoneWeapon(weaponId, location.getRegionID(), location.getX(), location.getY(), targetId)) + { + return 1; + } + return curSpeed; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/ShadowCrash.java b/src/main/java/com/attacktimer/VariableSpeed/ShadowCrash.java index 1ef9430..5805aba 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/ShadowCrash.java +++ b/src/main/java/com/attacktimer/VariableSpeed/ShadowCrash.java @@ -1,322 +1,322 @@ -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 lombok.extern.slf4j.Slf4j; -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; - -@Slf4j -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, final boolean debugLogs) - { - // 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 1 or less. It might also affect range but no-one uses - // that at yama so IDC. Another reason to prefer melee P3. - if (attackDelayHoldoffTicks <= 1 && isUsingMagic) - { - if (debugLogs) - { - log.debug("shadowCrash success, but magic and low CD"); - } - return 0; - } - if (debugLogs) - { - log.debug("shadowCrash success"); - } - 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; - } -} +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 lombok.extern.slf4j.Slf4j; +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; + +@Slf4j +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, final boolean debugLogs) + { + // 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 1 or less. It might also affect range but no-one uses + // that at yama so IDC. Another reason to prefer melee P3. + if (attackDelayHoldoffTicks <= 1 && isUsingMagic) + { + if (debugLogs) + { + log.debug("shadowCrash success, but magic and low CD"); + } + return 0; + } + if (debugLogs) + { + log.debug("shadowCrash success"); + } + 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 index c0c9906..65fa150 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/State/IStateTracker.java +++ b/src/main/java/com/attacktimer/VariableSpeed/State/IStateTracker.java @@ -1,72 +1,72 @@ -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; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; - -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) - {}; - - /** - * subscribe to when npcs are spawned - * - * @param npcSpawned the npc which has spawned - */ - default public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) - {}; - - /** - * subscribe to when npcs are despawned, note this is not when the NPC is dead but after the death - * animation has completed or out of render distance. - * - * @param npcDespawned the npc which has despawned - */ - default public void onNpcDespawned(final Client client, final NpcDespawned npcDespawned) - {}; -} +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; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; + +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) + {}; + + /** + * subscribe to when npcs are spawned + * + * @param npcSpawned the npc which has spawned + */ + default public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) + {}; + + /** + * subscribe to when npcs are despawned, note this is not when the NPC is dead but after the death + * animation has completed or out of render distance. + * + * @param npcDespawned the npc which has despawned + */ + default public void onNpcDespawned(final Client client, final NpcDespawned npcDespawned) + {}; +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/State/MarkOfDarkness.java b/src/main/java/com/attacktimer/VariableSpeed/State/MarkOfDarkness.java index e89d37c..78f6623 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/State/MarkOfDarkness.java +++ b/src/main/java/com/attacktimer/VariableSpeed/State/MarkOfDarkness.java @@ -1,89 +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; - } -} +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 index ee8f0ca..4a1c9de 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java +++ b/src/main/java/com/attacktimer/VariableSpeed/State/TickCount.java @@ -1,61 +1,61 @@ -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; - } - - @Override - 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; - } -} +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; + } + + @Override + 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 index 78e8496..91ac05f 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/State/Yama.java +++ b/src/main/java/com/attacktimer/VariableSpeed/State/Yama.java @@ -1,214 +1,214 @@ -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; -import net.runelite.api.events.NpcSpawned; -import net.runelite.api.gameval.NpcID; - -/** - * 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 YAMA_REGION_ID = 6045; - 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; - - @Override - 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; - } - } - - @Override - public void onChatMessage(Client client, ChatMessage event) - { - if (!inYamaRegion || yama == null) - { - return; - } - if (event.getMessage().startsWith("Your Yama success count is:")) - { - yama.killed(); - } - } - - @Override - public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) - { - if (!inYamaRegion) - { - return; - } - if (yama != null) - { - return; - } - final NPC npc = npcSpawned.getNpc(); - if (npc.getId() == NpcID.YAMA) - { - yama = new YamaData(npc); - } - } - - public static NPC isEitherVoidFlare(NPC a, NPC b) - { - if (a != null && a.getId() == NpcID.YAMA_VOIDFLARE) - { - return a; - } - - if (b != null && b.getId() == NpcID.YAMA_VOIDFLARE) - { - 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(); - } - } -} +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; +import net.runelite.api.events.NpcSpawned; +import net.runelite.api.gameval.NpcID; + +/** + * 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 YAMA_REGION_ID = 6045; + 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; + + @Override + 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; + } + } + + @Override + public void onChatMessage(Client client, ChatMessage event) + { + if (!inYamaRegion || yama == null) + { + return; + } + if (event.getMessage().startsWith("Your Yama success count is:")) + { + yama.killed(); + } + } + + @Override + public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) + { + if (!inYamaRegion) + { + return; + } + if (yama != null) + { + return; + } + final NPC npc = npcSpawned.getNpc(); + if (npc.getId() == NpcID.YAMA) + { + yama = new YamaData(npc); + } + } + + public static NPC isEitherVoidFlare(NPC a, NPC b) + { + if (a != null && a.getId() == NpcID.YAMA_VOIDFLARE) + { + return a; + } + + if (b != null && b.getId() == NpcID.YAMA_VOIDFLARE) + { + 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 index 79bb4dd..6ee156f 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/State/YamaPhase.java +++ b/src/main/java/com/attacktimer/VariableSpeed/State/YamaPhase.java @@ -1,45 +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; - } +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 b2b4280..bc80812 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/TombsOfAmascut.java +++ b/src/main/java/com/attacktimer/VariableSpeed/TombsOfAmascut.java @@ -1,55 +1,55 @@ -package com.attacktimer.VariableSpeed; - -/* - * 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.AttackType; -import com.attacktimer.ClientUtils.Utils; -import com.attacktimer.Spellbook; -import net.runelite.api.Client; -import net.runelite.api.gameval.NpcID; - -/** - * There is no cooldown when attacking the skulls with melee. - * - * https://oldschool.runescape.wiki/w/Energy_Siphon - */ -public class TombsOfAmascut implements IVariableSpeed -{ - 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 AttackType attkType = Utils.getAttackType(client); - if (targetId == NpcID.WARDENS_P3_ORB_BLUE && attkType.IsMelee()) - { - return 1; - } - return curSpeed; - } -} +package com.attacktimer.VariableSpeed; + +/* + * 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.AttackType; +import com.attacktimer.ClientUtils.Utils; +import com.attacktimer.Spellbook; +import net.runelite.api.Client; +import net.runelite.api.gameval.NpcID; + +/** + * There is no cooldown when attacking the skulls with melee. + * + * https://oldschool.runescape.wiki/w/Energy_Siphon + */ +public class TombsOfAmascut implements IVariableSpeed +{ + 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 AttackType attkType = Utils.getAttackType(client); + if (targetId == NpcID.WARDENS_P3_ORB_BLUE && attkType.IsMelee()) + { + return 1; + } + return curSpeed; + } +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java b/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java index f298548..e11434e 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java +++ b/src/main/java/com/attacktimer/VariableSpeed/TormentedDemons.java @@ -1,257 +1,257 @@ -package com.attacktimer.VariableSpeed; - -/* - * Copyright (c) 2024-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.AttackType; -import com.attacktimer.ClientUtils.Utils; -import com.attacktimer.Spellbook; -import com.attacktimer.VariableSpeed.State.TickCount; -import com.attacktimer.WeaponType; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import net.runelite.api.Client; -import net.runelite.api.NPC; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; -import net.runelite.api.gameval.NpcID; -import net.runelite.api.gameval.SpotanimID; - -/** - * TormentedDemons is the variable speed implementation for the "punish" attack a player can do against a - * demon. - * - * https://oldschool.runescape.wiki/w/Tormented_Demon/Strategies#Fire_bombs_and_shield - * > Within this moment of vulnerability, attacking the demon using crush weapons, heavy ranged weapons - * > (crossbows and ballistae), or casting a spell (not from a powered staff) increases all damage dealt by - * > X2−16, where X is the weapon's attack speed. The attack speed for this one attack is set to 4 ticks - * > (2.4s); if multiple demons are attacking, the attack speed decreases by one tick per additional demon - * > that is fighting the player. - * - * This class works by keeping a map which tracks every demon that the client is provided, then if any demon - * triggers a spot animation matching the vulnerability animation then we store this in the map. Every attack - * the parent {@see VariableSpeed} will call apply, in which if we are attacking a vulnerable demon we can do - * extra checks and compute the new attack speed and show the correct tick countdown. - */ -public class TormentedDemons implements IVariableSpeed -{ - private TickCount tickCount; - - TormentedDemons(TickCount tc) - { - this.tickCount = tc; - } - - 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); - if (!isTormentedDemon(targetId)) - { - return curSpeed; - } - final NPC target = Utils.getTargetNPC(client); - if (!tormentedDemons.containsKey(target)) - { - // This doesn't feel very likely but this is more graceful than throwing. - return curSpeed; - } - final DemonData targetDemon = tormentedDemons.get(target); - 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.get())) - { - return curSpeed; - } - // Finally the last checks, only certain attack styles and weapons can trigger the effect. - switch (atkType) - { - case POWERED_STAVE: - // Powered staves cannot trigger the effect - // TODO edge case with harm? - return curSpeed; - case MANUAL_AUTO_CAST: - break; - case MELEE_OR_RANGE: - final WeaponType wep = Utils.getWeaponType(client); - final AttackType attkType = Utils.getAttackType(client); - if (wep != WeaponType.HEAVY_RANGED && attkType != AttackType.CRUSH) - { - // This weapon is not a heavy ranged weapon or a melee weapon set to crush - // so it wont trigger the shorter cooldown. - return curSpeed; - } - break; - } - // All checks passed this is consuming the vulnerability, change our speed. - // - // TODO implement more than 1 demon cases - if (curSpeed > 4) - { - return 4; - } - else - { - return curSpeed; - } - } - - private static boolean isTormentedDemon(int targetId) - { - return targetId == NpcID.TORMENTED_DEMON_1 || targetId == NpcID.TORMENTED_DEMON_2; - } - - private Map tormentedDemons = new HashMap(); - - @Override - public void onGameTick(Client client, GameTick tick) - { - for (final Entry td : tormentedDemons.entrySet()) - { - final boolean isVulnerable = td.getKey().hasSpotAnim(SpotanimID.LUC2_UNDEAD_DEMON_EXPLOSION_FIRE_SPOT); - td.getValue().update(tickCount.get(), isVulnerable); - } - } - - @Override - public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) - { - final NPC npc = npcSpawned.getNpc(); - maybeInsertDemon(npc); - } - - private void maybeInsertDemon(final NPC npc) - { - if (isTormentedDemon(npc.getId())) - { - final boolean isVulnerable = npc.hasSpotAnim(SpotanimID.LUC2_UNDEAD_DEMON_EXPLOSION_FIRE_SPOT); - tormentedDemons.put(npc, new DemonData(tickCount.get(), isVulnerable)); - } - } - - @Override - public void onNpcDespawned(final Client client, final NpcDespawned npcDespawned) - { - final NPC npc = npcDespawned.getNpc(); - if (isTormentedDemon(npc.getId())) - { - tormentedDemons.remove(npc); - } - } - - /** - * DemonData is an internal helper class containing the last ticks in which a demon was noticed by the - * client, the tick it was vulnerable (if ever) and the tick in which it was attacked while vulnerable. - */ - class DemonData - { - // VulTicksAfterEnd is just a guess the wiki isn't clear how long this period is, from testing 10 - // ticks feels about right. - private static final int VULN_TICKS_AFTER_END = 10; - private Integer vulnerableStart; - private Integer vulnerableFinish; - private int attacked; - - DemonData(int tick, boolean vuln) - { - this.update(tick, vuln); - this.attacked = -1; - } - - void update(int tick, boolean vuln) - { - // NOTE: we can't use the chat message "The demon's spell binds you" because this doesn't trigger - // at the start of any kill. - if (vuln && this.vulnerableStart == null) - { - this.vulnerableStart = Integer.valueOf(tick); - this.vulnerableFinish = Integer.valueOf(tick); - } - else if (vuln && this.vulnerableStart != null) - { - this.vulnerableFinish = Integer.valueOf(tick); - } - - if (!vuln) - { - this.vulnerableStart = null; - } - if (this.attacked != -1 && this.attacked + 1 >= tick) - { - this.attacked = -1; - } - } - - boolean isVulnerable(int tick) - { - if (this.vulnerableFinish == null) - { - return false; - } - return (this.vulnerableFinish + VULN_TICKS_AFTER_END) > tick; - } - - // vulnConsumed returns true if the demon was already attack, false if not and the vuln is still usable - boolean vulnConsumed(int tick) - { - if (this.attacked == tick || this.attacked == -1) - { - this.consumeVuln(tick); - return false; - } - else if (this.attacked > tick + VULN_TICKS_AFTER_END) - { - // already attacked within the window don't consume the vuln again we let update handle this - return true; - } - else - { - this.consumeVuln(tick); - return false; - } - } - - void consumeVuln(int tick) - { - this.attacked = tick; - } - - @Override - public String toString() - { - return "vulnerableStart: " + String.valueOf(this.vulnerableStart) + " vulnerableFinish: " + String.valueOf(this.vulnerableFinish); - } - } - -} +package com.attacktimer.VariableSpeed; + +/* + * Copyright (c) 2024-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.AttackType; +import com.attacktimer.ClientUtils.Utils; +import com.attacktimer.Spellbook; +import com.attacktimer.VariableSpeed.State.TickCount; +import com.attacktimer.WeaponType; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +import net.runelite.api.gameval.NpcID; +import net.runelite.api.gameval.SpotanimID; + +/** + * TormentedDemons is the variable speed implementation for the "punish" attack a player can do against a + * demon. + * + * https://oldschool.runescape.wiki/w/Tormented_Demon/Strategies#Fire_bombs_and_shield + * > Within this moment of vulnerability, attacking the demon using crush weapons, heavy ranged weapons + * > (crossbows and ballistae), or casting a spell (not from a powered staff) increases all damage dealt by + * > X2−16, where X is the weapon's attack speed. The attack speed for this one attack is set to 4 ticks + * > (2.4s); if multiple demons are attacking, the attack speed decreases by one tick per additional demon + * > that is fighting the player. + * + * This class works by keeping a map which tracks every demon that the client is provided, then if any demon + * triggers a spot animation matching the vulnerability animation then we store this in the map. Every attack + * the parent {@see VariableSpeed} will call apply, in which if we are attacking a vulnerable demon we can do + * extra checks and compute the new attack speed and show the correct tick countdown. + */ +public class TormentedDemons implements IVariableSpeed +{ + private TickCount tickCount; + + TormentedDemons(TickCount tc) + { + this.tickCount = tc; + } + + 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); + if (!isTormentedDemon(targetId)) + { + return curSpeed; + } + final NPC target = Utils.getTargetNPC(client); + if (!tormentedDemons.containsKey(target)) + { + // This doesn't feel very likely but this is more graceful than throwing. + return curSpeed; + } + final DemonData targetDemon = tormentedDemons.get(target); + 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.get())) + { + return curSpeed; + } + // Finally the last checks, only certain attack styles and weapons can trigger the effect. + switch (atkType) + { + case POWERED_STAVE: + // Powered staves cannot trigger the effect + // TODO edge case with harm? + return curSpeed; + case MANUAL_AUTO_CAST: + break; + case MELEE_OR_RANGE: + final WeaponType wep = Utils.getWeaponType(client); + final AttackType attkType = Utils.getAttackType(client); + if (wep != WeaponType.HEAVY_RANGED && attkType != AttackType.CRUSH) + { + // This weapon is not a heavy ranged weapon or a melee weapon set to crush + // so it wont trigger the shorter cooldown. + return curSpeed; + } + break; + } + // All checks passed this is consuming the vulnerability, change our speed. + // + // TODO implement more than 1 demon cases + if (curSpeed > 4) + { + return 4; + } + else + { + return curSpeed; + } + } + + private static boolean isTormentedDemon(int targetId) + { + return targetId == NpcID.TORMENTED_DEMON_1 || targetId == NpcID.TORMENTED_DEMON_2; + } + + private Map tormentedDemons = new HashMap(); + + @Override + public void onGameTick(Client client, GameTick tick) + { + for (final Entry td : tormentedDemons.entrySet()) + { + final boolean isVulnerable = td.getKey().hasSpotAnim(SpotanimID.LUC2_UNDEAD_DEMON_EXPLOSION_FIRE_SPOT); + td.getValue().update(tickCount.get(), isVulnerable); + } + } + + @Override + public void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) + { + final NPC npc = npcSpawned.getNpc(); + maybeInsertDemon(npc); + } + + private void maybeInsertDemon(final NPC npc) + { + if (isTormentedDemon(npc.getId())) + { + final boolean isVulnerable = npc.hasSpotAnim(SpotanimID.LUC2_UNDEAD_DEMON_EXPLOSION_FIRE_SPOT); + tormentedDemons.put(npc, new DemonData(tickCount.get(), isVulnerable)); + } + } + + @Override + public void onNpcDespawned(final Client client, final NpcDespawned npcDespawned) + { + final NPC npc = npcDespawned.getNpc(); + if (isTormentedDemon(npc.getId())) + { + tormentedDemons.remove(npc); + } + } + + /** + * DemonData is an internal helper class containing the last ticks in which a demon was noticed by the + * client, the tick it was vulnerable (if ever) and the tick in which it was attacked while vulnerable. + */ + class DemonData + { + // VulTicksAfterEnd is just a guess the wiki isn't clear how long this period is, from testing 10 + // ticks feels about right. + private static final int VULN_TICKS_AFTER_END = 10; + private Integer vulnerableStart; + private Integer vulnerableFinish; + private int attacked; + + DemonData(int tick, boolean vuln) + { + this.update(tick, vuln); + this.attacked = -1; + } + + void update(int tick, boolean vuln) + { + // NOTE: we can't use the chat message "The demon's spell binds you" because this doesn't trigger + // at the start of any kill. + if (vuln && this.vulnerableStart == null) + { + this.vulnerableStart = Integer.valueOf(tick); + this.vulnerableFinish = Integer.valueOf(tick); + } + else if (vuln && this.vulnerableStart != null) + { + this.vulnerableFinish = Integer.valueOf(tick); + } + + if (!vuln) + { + this.vulnerableStart = null; + } + if (this.attacked != -1 && this.attacked + 1 >= tick) + { + this.attacked = -1; + } + } + + boolean isVulnerable(int tick) + { + if (this.vulnerableFinish == null) + { + return false; + } + return (this.vulnerableFinish + VULN_TICKS_AFTER_END) > tick; + } + + // vulnConsumed returns true if the demon was already attack, false if not and the vuln is still usable + boolean vulnConsumed(int tick) + { + if (this.attacked == tick || this.attacked == -1) + { + this.consumeVuln(tick); + return false; + } + else if (this.attacked > tick + VULN_TICKS_AFTER_END) + { + // already attacked within the window don't consume the vuln again we let update handle this + return true; + } + else + { + this.consumeVuln(tick); + return false; + } + } + + void consumeVuln(int tick) + { + this.attacked = tick; + } + + @Override + public String toString() + { + return "vulnerableStart: " + String.valueOf(this.vulnerableStart) + " vulnerableFinish: " + String.valueOf(this.vulnerableFinish); + } + } + +} diff --git a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java index eeba750..8065129 100644 --- a/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java +++ b/src/main/java/com/attacktimer/VariableSpeed/VariableSpeed.java @@ -1,140 +1,140 @@ -package com.attacktimer.VariableSpeed; - -/* - * Copyright (c) 2024-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.AttackTimerMetronomePlugin; -import com.attacktimer.Spellbook; -import com.attacktimer.VariableSpeed.State.IStateTracker; -import com.attacktimer.VariableSpeed.State.MarkOfDarkness; -import com.attacktimer.VariableSpeed.State.Yama; -import net.runelite.api.Client; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; - -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. - */ - public static int compute(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, - final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed) - { - int newSpeed = baseSpeed; - for (final IVariableSpeed i : TO_APPLY) - { - newSpeed = i.apply(client, curAnimation, atkType, spellbook, damageDealt, lastSpecDelta, baseSpeed, newSpeed); - } - return newSpeed; - } - - public static void onGameTick(final Client client, final GameTick tick) - { - for (final IStateTracker i : TO_TRACK) - { - i.onGameTick(client, tick); - } - for (final IStateTracker i : TO_APPLY) - { - i.onGameTick(client, tick); - } - } - - public static void onChatMessage(final Client client, final ChatMessage event) - { - for (final IStateTracker i : TO_TRACK) - { - i.onChatMessage(client, event); - } - for (final IStateTracker i : TO_APPLY) - { - i.onChatMessage(client, event); - } - } - - public static void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) - { - for (final IStateTracker i : TO_TRACK) - { - i.onNpcSpawned(client, npcSpawned); - } - for (final IStateTracker i : TO_APPLY) - { - i.onNpcSpawned(client, npcSpawned); - } - } - - public static void onNpcDespawned(final Client client, final NpcDespawned npcDespawned) - { - for (final IStateTracker i : TO_TRACK) - { - i.onNpcDespawned(client, npcDespawned); - } - for (final IStateTracker i : TO_APPLY) - { - i.onNpcDespawned(client, npcDespawned); - } - } - - private static final Yama YAMA = new Yama(); - private static final MarkOfDarkness MARK_OF_DARKNESS = new MarkOfDarkness(); - - 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. - AttackTimerMetronomePlugin.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. - // Then overriding speeds last, which set a speed. - - // Incremental: - new BloodMoonSet(), - new RapidAttackStyle(), - new RedKerisSpec(), - new PurgingStaffSpec(YAMA), - new EyeOfAyak(), - new TormentedDemons(AttackTimerMetronomePlugin.TC), - new RoyalTitans(), - - // Overriding modifiers: - new Amoxliatl(), - 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, AttackTimerMetronomePlugin.TC); - -} +package com.attacktimer.VariableSpeed; + +/* + * Copyright (c) 2024-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.AttackTimerMetronomePlugin; +import com.attacktimer.Spellbook; +import com.attacktimer.VariableSpeed.State.IStateTracker; +import com.attacktimer.VariableSpeed.State.MarkOfDarkness; +import com.attacktimer.VariableSpeed.State.Yama; +import net.runelite.api.Client; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; + +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. + */ + public static int compute(final Client client, final AnimationData curAnimation, final AttackProcedure atkType, + final Spellbook spellbook, final int damageDealt, final int lastSpecDelta, final int baseSpeed) + { + int newSpeed = baseSpeed; + for (final IVariableSpeed i : TO_APPLY) + { + newSpeed = i.apply(client, curAnimation, atkType, spellbook, damageDealt, lastSpecDelta, baseSpeed, newSpeed); + } + return newSpeed; + } + + public static void onGameTick(final Client client, final GameTick tick) + { + for (final IStateTracker i : TO_TRACK) + { + i.onGameTick(client, tick); + } + for (final IStateTracker i : TO_APPLY) + { + i.onGameTick(client, tick); + } + } + + public static void onChatMessage(final Client client, final ChatMessage event) + { + for (final IStateTracker i : TO_TRACK) + { + i.onChatMessage(client, event); + } + for (final IStateTracker i : TO_APPLY) + { + i.onChatMessage(client, event); + } + } + + public static void onNpcSpawned(final Client client, final NpcSpawned npcSpawned) + { + for (final IStateTracker i : TO_TRACK) + { + i.onNpcSpawned(client, npcSpawned); + } + for (final IStateTracker i : TO_APPLY) + { + i.onNpcSpawned(client, npcSpawned); + } + } + + public static void onNpcDespawned(final Client client, final NpcDespawned npcDespawned) + { + for (final IStateTracker i : TO_TRACK) + { + i.onNpcDespawned(client, npcDespawned); + } + for (final IStateTracker i : TO_APPLY) + { + i.onNpcDespawned(client, npcDespawned); + } + } + + private static final Yama YAMA = new Yama(); + private static final MarkOfDarkness MARK_OF_DARKNESS = new MarkOfDarkness(); + + 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. + AttackTimerMetronomePlugin.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. + // Then overriding speeds last, which set a speed. + + // Incremental: + new BloodMoonSet(), + new RapidAttackStyle(), + new RedKerisSpec(), + new PurgingStaffSpec(YAMA), + new EyeOfAyak(), + new TormentedDemons(AttackTimerMetronomePlugin.TC), + new RoyalTitans(), + + // Overriding modifiers: + new Amoxliatl(), + 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, AttackTimerMetronomePlugin.TC); + +} diff --git a/src/test/java/com/attacktimer/BasicTests.java b/src/test/java/com/attacktimer/BasicTests.java index adf2161..b63afba 100644 --- a/src/test/java/com/attacktimer/BasicTests.java +++ b/src/test/java/com/attacktimer/BasicTests.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 static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; - -import com.attacktimer.AttackTimerMetronomePlugin.AttackState; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import java.nio.file.Paths; -import net.runelite.api.ChatMessageType; -import net.runelite.api.Player; -import net.runelite.api.events.ChatMessage; -import org.junit.Test; - -public class BasicTests extends IntegrationTests -{ - @Test - public void basicTest() throws Exception - { - ByteArrayDataOutput channel = ByteStreams.newDataOutput(); - underTest.writeState(channel); - // Trivial Pre-conditions: - assertSame(AttackState.NOT_ATTACKING, underTest.attackState); - - // Basic test case: - // 1. Start by setting up the player - // 2. Mock an attack animation - // 3. Check that the plugin has registered the attack - // 4. Check that the plugin counts down correctly - // 5. Check that the plugin is back to a waiting state and it still counts down - - writeTestMessage("1. Start by setting up the player and plugin", channel); - int atkSpeed = 3; // no weapon equipped (4 ticks, plugin starts counting from 3) - int tick = 0; - Player mockedPlayer = pluginMockSetup(); - when(mockedClient.getTickCount()).thenReturn(tick); - - writeTestMessage("2. Mock an attack animation", channel); - - // set the animation to an attack - when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); - // tell the plugin that a tick occurred - onGameTick(channel); - - writeTestMessage("3. Check that the plugin has registered the attack", channel); - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - - // clear the animation - when(mockedPlayer.getAnimation()).thenReturn(NO_ANIMATION); - - writeTestMessage("4. Check that the plugin counts down correctly", channel); - while (atkSpeed > 0) - { - tick++; - atkSpeed--; - when(mockedClient.getTickCount()).thenReturn(tick); - onGameTick(channel); - assertSame(AttackState.DELAYED, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - } - - writeTestMessage("5. Check that the plugin is back to a waiting state and it still counts down", channel); - tick++; - onGameTick(channel); - assertSame(AttackState.NOT_ATTACKING, underTest.attackState); - while (tick < 30) - { - tick++; - when(mockedClient.getTickCount()).thenReturn(tick); - onGameTick(channel); - assertSame(AttackState.NOT_ATTACKING, underTest.attackState); - assertTrue(underTest.attackDelayHoldoffTicks < 0); // hold off should go negative - } - - performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + "basicTest.txt")); - } - - @Test - public void eatingFoodTest() throws Exception - { - ByteArrayDataOutput channel = ByteStreams.newDataOutput(); - underTest.writeState(channel); - // Trivial Pre-conditions: - assertSame(AttackState.NOT_ATTACKING, underTest.attackState); - writeTestMessage("1. Start by setting up the player and plugin", channel); - int atkSpeed = 3; // no weapon equipped (4 ticks, plugin starts counting from 3) - int tick = 0; - Player mockedPlayer = pluginMockSetup(); - when(mockedClient.getTickCount()).thenReturn(tick); - - writeTestMessage("2. Mock an attack animation", channel); - - // set the animation to an attack - when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); - // tell the plugin that a tick occurred - onGameTick(channel); - - writeTestMessage("3. Check that the plugin has registered the attack", channel); - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - - when(mockedPlayer.getAnimation()).thenReturn(NO_ANIMATION); - underTest.writeState(channel); - - writeTestMessage("Perform an eat", channel); - ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the shark.", "", 0); - underTest.onChatMessage(chatMessage); - // the plugin wont apply the delay directly it waits until the next game tick - assertSame(3, underTest.pendingEatDelayTicks); - // Should still be same state - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - underTest.writeState(channel); - - // Subsequent values should be offset by the eat delay - writeTestMessage("Next game tick", channel); - atkSpeed += 2; - onGameTick(channel); - assertSame(AttackState.DELAYED, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - - // Note that this is essentially a "double" eat test. - writeTestMessage("Perform a fast eat", channel); - chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the halibut.", "", 0); - underTest.onChatMessage(chatMessage); - // the plugin wont apply the delay directly it waits until the next game tick - assertSame(2, underTest.pendingEatDelayTicks); - // Should still be same state - assertSame(AttackState.DELAYED, underTest.attackState); - underTest.writeState(channel); - - // Subsequent values should be offset by the eat delay - writeTestMessage("Next game tick", channel); - atkSpeed += 1; - onGameTick(channel); - assertSame(AttackState.DELAYED, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - - writeTestMessage("4. Check that the plugin counts down correctly", channel); - while (atkSpeed > 0) - { - tick++; - atkSpeed--; - when(mockedClient.getTickCount()).thenReturn(tick); - onGameTick(channel); - assertSame(AttackState.DELAYED, underTest.attackState); - assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); - } - - performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + "eatingFoodTest.txt")); - } -} +package com.attacktimer; + +/* + * Copyright (c) 2026, Lexer747 + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import com.attacktimer.AttackTimerMetronomePlugin.AttackState; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import java.nio.file.Paths; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Player; +import net.runelite.api.events.ChatMessage; +import org.junit.Test; + +public class BasicTests extends IntegrationTests +{ + @Test + public void basicTest() throws Exception + { + ByteArrayDataOutput channel = ByteStreams.newDataOutput(); + underTest.writeState(channel); + // Trivial Pre-conditions: + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + + // Basic test case: + // 1. Start by setting up the player + // 2. Mock an attack animation + // 3. Check that the plugin has registered the attack + // 4. Check that the plugin counts down correctly + // 5. Check that the plugin is back to a waiting state and it still counts down + + writeTestMessage("1. Start by setting up the player and plugin", channel); + int atkSpeed = 3; // no weapon equipped (4 ticks, plugin starts counting from 3) + int tick = 0; + Player mockedPlayer = pluginMockSetup(); + when(mockedClient.getTickCount()).thenReturn(tick); + + writeTestMessage("2. Mock an attack animation", channel); + + // set the animation to an attack + when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); + // tell the plugin that a tick occurred + onGameTick(channel); + + writeTestMessage("3. Check that the plugin has registered the attack", channel); + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + + // clear the animation + when(mockedPlayer.getAnimation()).thenReturn(NO_ANIMATION); + + writeTestMessage("4. Check that the plugin counts down correctly", channel); + while (atkSpeed > 0) + { + tick++; + atkSpeed--; + when(mockedClient.getTickCount()).thenReturn(tick); + onGameTick(channel); + assertSame(AttackState.DELAYED, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + } + + writeTestMessage("5. Check that the plugin is back to a waiting state and it still counts down", channel); + tick++; + onGameTick(channel); + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + while (tick < 30) + { + tick++; + when(mockedClient.getTickCount()).thenReturn(tick); + onGameTick(channel); + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + assertTrue(underTest.attackDelayHoldoffTicks < 0); // hold off should go negative + } + + performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + "basicTest.txt")); + } + + @Test + public void eatingFoodTest() throws Exception + { + ByteArrayDataOutput channel = ByteStreams.newDataOutput(); + underTest.writeState(channel); + // Trivial Pre-conditions: + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + writeTestMessage("1. Start by setting up the player and plugin", channel); + int atkSpeed = 3; // no weapon equipped (4 ticks, plugin starts counting from 3) + int tick = 0; + Player mockedPlayer = pluginMockSetup(); + when(mockedClient.getTickCount()).thenReturn(tick); + + writeTestMessage("2. Mock an attack animation", channel); + + // set the animation to an attack + when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); + // tell the plugin that a tick occurred + onGameTick(channel); + + writeTestMessage("3. Check that the plugin has registered the attack", channel); + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + + when(mockedPlayer.getAnimation()).thenReturn(NO_ANIMATION); + underTest.writeState(channel); + + writeTestMessage("Perform an eat", channel); + ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the shark.", "", 0); + underTest.onChatMessage(chatMessage); + // the plugin wont apply the delay directly it waits until the next game tick + assertSame(3, underTest.pendingEatDelayTicks); + // Should still be same state + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + underTest.writeState(channel); + + // Subsequent values should be offset by the eat delay + writeTestMessage("Next game tick", channel); + atkSpeed += 2; + onGameTick(channel); + assertSame(AttackState.DELAYED, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + + // Note that this is essentially a "double" eat test. + writeTestMessage("Perform a fast eat", channel); + chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the halibut.", "", 0); + underTest.onChatMessage(chatMessage); + // the plugin wont apply the delay directly it waits until the next game tick + assertSame(2, underTest.pendingEatDelayTicks); + // Should still be same state + assertSame(AttackState.DELAYED, underTest.attackState); + underTest.writeState(channel); + + // Subsequent values should be offset by the eat delay + writeTestMessage("Next game tick", channel); + atkSpeed += 1; + onGameTick(channel); + assertSame(AttackState.DELAYED, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + + writeTestMessage("4. Check that the plugin counts down correctly", channel); + while (atkSpeed > 0) + { + tick++; + atkSpeed--; + when(mockedClient.getTickCount()).thenReturn(tick); + onGameTick(channel); + assertSame(AttackState.DELAYED, underTest.attackState); + assertSame(atkSpeed, underTest.attackDelayHoldoffTicks); + } + + performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + "eatingFoodTest.txt")); + } +} diff --git a/src/test/java/com/attacktimer/EatTest.java b/src/test/java/com/attacktimer/EatTest.java index 16a9e97..1745b89 100644 --- a/src/test/java/com/attacktimer/EatTest.java +++ b/src/test/java/com/attacktimer/EatTest.java @@ -1,83 +1,83 @@ -package com.attacktimer; - -/* - * Copyright (c) 2026, Lexer747 - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.when; - -import com.attacktimer.AttackTimerMetronomePlugin.AttackState; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import net.runelite.api.ChatMessageType; -import net.runelite.api.Player; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.GameTick; -import org.junit.Test; - -public class EatTest extends IntegrationTests -{ - @Test - public void ExhaustiveEatTest() throws Exception - { - Player mockedPlayer = pluginMockSetup(); - when(mockedClient.getTickCount()).thenReturn(0); - when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); - underTest.onGameTick(new GameTick()); - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - assertSame(3, underTest.attackDelayHoldoffTicks); - String[] fastFoodsToTest = { - // NOTE case sensitivity here is import this should reflect in-game so that the plugin is resilient - // to mixed casing - "Karambwan", "halibut", "choc chip crunchies", - }; - String[] foodsToTest = { - "shark", "meat pizza", "A brand new food message", "sunlight antelope", "moonlight antelope", - "purple sweets", - }; - var curEatDelayTicks = underTest.pendingEatDelayTicks; - for (String food : foodsToTest) - { - ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the " + food + ".", "", - 0); - underTest.onChatMessage(chatMessage); - curEatDelayTicks += 3; - assertSame(curEatDelayTicks, underTest.pendingEatDelayTicks); - } - - for (String food : fastFoodsToTest) - { - ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the " + food + ".", "", - 0); - underTest.onChatMessage(chatMessage); - curEatDelayTicks += 2; - assertSame(curEatDelayTicks, underTest.pendingEatDelayTicks); - } - // We don't care about this output for the test but want the mock to be happy this test is actually - // fetching the state. - ByteArrayDataOutput channel = ByteStreams.newDataOutput(); - underTest.writeState(channel); - } -} +package com.attacktimer; + +/* + * Copyright (c) 2026, Lexer747 + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.when; + +import com.attacktimer.AttackTimerMetronomePlugin.AttackState; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Player; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameTick; +import org.junit.Test; + +public class EatTest extends IntegrationTests +{ + @Test + public void ExhaustiveEatTest() throws Exception + { + Player mockedPlayer = pluginMockSetup(); + when(mockedClient.getTickCount()).thenReturn(0); + when(mockedPlayer.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); + underTest.onGameTick(new GameTick()); + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + assertSame(3, underTest.attackDelayHoldoffTicks); + String[] fastFoodsToTest = { + // NOTE case sensitivity here is import this should reflect in-game so that the plugin is resilient + // to mixed casing + "Karambwan", "halibut", "choc chip crunchies", + }; + String[] foodsToTest = { + "shark", "meat pizza", "A brand new food message", "sunlight antelope", "moonlight antelope", + "purple sweets", + }; + var curEatDelayTicks = underTest.pendingEatDelayTicks; + for (String food : foodsToTest) + { + ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the " + food + ".", "", + 0); + underTest.onChatMessage(chatMessage); + curEatDelayTicks += 3; + assertSame(curEatDelayTicks, underTest.pendingEatDelayTicks); + } + + for (String food : fastFoodsToTest) + { + ChatMessage chatMessage = new ChatMessage(null, ChatMessageType.SPAM, "", "You eat the " + food + ".", "", + 0); + underTest.onChatMessage(chatMessage); + curEatDelayTicks += 2; + assertSame(curEatDelayTicks, underTest.pendingEatDelayTicks); + } + // We don't care about this output for the test but want the mock to be happy this test is actually + // fetching the state. + ByteArrayDataOutput channel = ByteStreams.newDataOutput(); + underTest.writeState(channel); + } +} diff --git a/src/test/java/com/attacktimer/README.md b/src/test/java/com/attacktimer/README.md index b68bdf5..6f1c88c 100644 --- a/src/test/java/com/attacktimer/README.md +++ b/src/test/java/com/attacktimer/README.md @@ -1,35 +1,35 @@ -# Testing Approach - -Instead of all testing being done in game in the client and capturing the results as screen shots or videos, -we can instead programmatically mock out scenarios and concretely assert that the plugin state is as expected. - -This can be done via the `performStateVerificationOrUpdate` function which will assert that the plugin state -and test messages are expected. These expectations can be created programmatically too, essentially marking -them as "golden" and verified to be working when they were added. This has better guarantees that more -unexpected state is asserted on, which will help reduce breakages. However it will also produce more -false-negatives as too much state is captured. This trade-off is worth it in my eyes. - -Finally because the test expectations are human plain text, they're easy to digest and diff between revisions. - -## How do I run the tests? - -``` -gradlew test -``` - -You should see something like: -``` --------------------------------------------------------------------- -| Results: SUCCESS (4 tests, 3 successes, 0 failures, 1 skipped) | --------------------------------------------------------------------- -``` - -If all the tests are passing. - -## How do I update test expectations? - -``` -gradlew test -Pupdate=all -``` - +# Testing Approach + +Instead of all testing being done in game in the client and capturing the results as screen shots or videos, +we can instead programmatically mock out scenarios and concretely assert that the plugin state is as expected. + +This can be done via the `performStateVerificationOrUpdate` function which will assert that the plugin state +and test messages are expected. These expectations can be created programmatically too, essentially marking +them as "golden" and verified to be working when they were added. This has better guarantees that more +unexpected state is asserted on, which will help reduce breakages. However it will also produce more +false-negatives as too much state is captured. This trade-off is worth it in my eyes. + +Finally because the test expectations are human plain text, they're easy to digest and diff between revisions. + +## How do I run the tests? + +``` +gradlew test +``` + +You should see something like: +``` +-------------------------------------------------------------------- +| Results: SUCCESS (4 tests, 3 successes, 0 failures, 1 skipped) | +-------------------------------------------------------------------- +``` + +If all the tests are passing. + +## How do I update test expectations? + +``` +gradlew test -Pupdate=all +``` + You should now see a dirty working dir as the new expectations will be written to your disk. \ No newline at end of file diff --git a/src/test/java/com/attacktimer/RoyalTitansTest.java b/src/test/java/com/attacktimer/RoyalTitansTest.java index 9c97cc3..0aee01c 100644 --- a/src/test/java/com/attacktimer/RoyalTitansTest.java +++ b/src/test/java/com/attacktimer/RoyalTitansTest.java @@ -1,304 +1,304 @@ -package com.attacktimer; - -/* - * Copyright (c) 2026, Lexer747 - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.attacktimer.AttackTimerMetronomePlugin.AttackState; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import java.io.IOException; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.Set; -import net.runelite.api.NPC; -import net.runelite.api.Player; -import net.runelite.api.Skill; -import net.runelite.api.WorldView; -import net.runelite.api.coords.LocalPoint; -import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.FakeXpDrop; -import net.runelite.api.events.NpcDespawned; -import net.runelite.api.events.NpcSpawned; -import org.junit.Test; - -public class RoyalTitansTest extends IntegrationTests -{ - @Test - public void SingleKillManualCast() throws Exception - { - int expected = 3; - final int xp = 60; - - ByteArrayDataOutput channel = ByteStreams.newDataOutput(); - underTest.writeState(channel); - - writeTestMessage("distance 1", channel); - runSingleTest(channel, 1, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 2", channel); - runSingleTest(channel, 2, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 3", channel); - runSingleTest(channel, 3, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 4", channel); - runSingleTest(channel, 4, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 5", channel); - runSingleTest(channel, 5, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 6", channel); - runSingleTest(channel, 6, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 7", channel); - runSingleTest(channel, 7, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 8", channel); - runSingleTest(channel, 8, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - expected = 4; - writeTestMessage("distance 9", channel); - runSingleTest(channel, 9, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 10", channel); - runSingleTest(channel, 10, expected, xp); - - performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + "SingleKillManualCast.txt")); - } - - private void runSingleTest(ByteArrayDataOutput channel, int distance, int expected, int xp) - throws Exception, IOException - { - final Player player = pluginMockSetup(); - // Ensure we're using correct magic - when(player.getAnimation()).thenReturn(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF.animationId); - - final var it = iceElementals.iterator(); - final NPC target = it.next(); - // Set up hit delay by setting distance - final var bounds = getWorldForRegionId(11669); - final var wp = new WorldPoint(bounds.minX + 128, bounds.minY + 128, 0); - when(target.getWorldLocation()).thenReturn(wp); - when(player.getWorldLocation()).thenReturn(wp.dx(distance)); - when(player.getInteracting()).thenReturn(target); - while (it.hasNext()) - { - final var splashed = it.next(); - // need to mock all the other elementals, ensure they are off screen: - when(splashed.getWorldLocation()).thenReturn(wp.dx(128)); - } - - // Ensure we deal enough damage - underTest.onFakeXpDrop(new FakeXpDrop(Skill.HITPOINTS, xp)); - - onGameTick(channel); - - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - assertSame(expected, underTest.attackDelayHoldoffTicks); - } - - @Test - public void AoEKillManualCast() throws Exception - { - // We start quicker than the single kills and expect decay as they get further away due to hit delay - int expected = 1; - final int xp = 60; - - ByteArrayDataOutput channel = ByteStreams.newDataOutput(); - underTest.writeState(channel); - - writeTestMessage("distance 1", channel); - runAoETest(channel, 1, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 2", channel); - runAoETest(channel, 2, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - expected = 2; - writeTestMessage("distance 3", channel); - runAoETest(channel, 3, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 4", channel); - runAoETest(channel, 4, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 5", channel); - runAoETest(channel, 5, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - expected = 3; - writeTestMessage("distance 6", channel); - runAoETest(channel, 6, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 7", channel); - runAoETest(channel, 7, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 8", channel); - runAoETest(channel, 8, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - expected = 4; - writeTestMessage("distance 9", channel); - runAoETest(channel, 9, expected, xp); - for (int i = 0; i < expected; i++) - { - onGameTick(channel); - } - writeTestMessage("distance 10", channel); - runAoETest(channel, 10, expected, xp); - - performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + "AoEKillManualCast.txt")); - } - - private void runAoETest(ByteArrayDataOutput channel, int distance, int expected, int xp) - throws Exception, IOException - { - final Player player = pluginMockSetup(); - // Ensure we're using correct magic - when(player.getAnimation()).thenReturn(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF.animationId); - - when(mockedClient.getLocalPlayer()).thenReturn(player); - final var it = iceElementals.iterator(); - final NPC target = it.next(); - // Set up hit delay by setting distance - final var bounds = getWorldForRegionId(11669); - final var wp = new WorldPoint(bounds.minX + 128, bounds.minY + 128, 0); - when(target.getWorldLocation()).thenReturn(wp); - when(player.getWorldLocation()).thenReturn(wp.dx(distance)); - when(player.getInteracting()).thenReturn(target); - while (it.hasNext()) - { - final var splashed = it.next(); - when(splashed.getWorldLocation()).thenReturn(wp); - } - - // Ensure we deal enough damage - underTest.onFakeXpDrop(new FakeXpDrop(Skill.HITPOINTS, xp)); - - onGameTick(channel); - - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - assertSame(expected, underTest.attackDelayHoldoffTicks); - - // clean up: - for (final var elemental : iceElementals) - { - underTest.onNpcDespawned(new NpcDespawned(elemental)); - } - } - - @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); - - // need some extra mocks to stop the plugin running into an exception on the - // client APIs - // -- Mock World - - WorldView mockedWorldView = mock(WorldView.class); - when(mockedWorldView.isInstance()).thenReturn(true); - when(mockedWorldView.getInstanceTemplateChunks()).thenReturn(createInstanceTemplateChunks(11669)); - int mockedPlane = 0; - LocalPoint localPoint = LocalPoint.fromScene(30, 30, mockedWorldView); - when(mockedClient.getLocalPlayer()).thenReturn(mockedPlayer); - when(mockedPlayer.getLocalLocation()).thenReturn(localPoint); - when(mockedClient.getTopLevelWorldView()).thenReturn(mockedWorldView); - when(mockedClient.getWorldView(0)).thenReturn(mockedWorldView); - when(mockedWorldView.getPlane()).thenReturn(mockedPlane); - - // Finally turn the plugin "on" - underTest.startUp(); - - // Create the elementals - for (int i = 0; i < 3; i++) - { - final var ice = mock(NPC.class); - iceElementals.add(ice); - setNPCMock(ice, 14151); - underTest.onNpcSpawned(new NpcSpawned(ice)); - } - - return mockedPlayer; - } - - protected Set iceElementals = new HashSet(); - protected Set fireElementals = new HashSet(); -} +package com.attacktimer; + +/* + * Copyright (c) 2026, Lexer747 + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.attacktimer.AttackTimerMetronomePlugin.AttackState; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Set; +import net.runelite.api.NPC; +import net.runelite.api.Player; +import net.runelite.api.Skill; +import net.runelite.api.WorldView; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.FakeXpDrop; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +import org.junit.Test; + +public class RoyalTitansTest extends IntegrationTests +{ + @Test + public void SingleKillManualCast() throws Exception + { + int expected = 3; + final int xp = 60; + + ByteArrayDataOutput channel = ByteStreams.newDataOutput(); + underTest.writeState(channel); + + writeTestMessage("distance 1", channel); + runSingleTest(channel, 1, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 2", channel); + runSingleTest(channel, 2, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 3", channel); + runSingleTest(channel, 3, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 4", channel); + runSingleTest(channel, 4, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 5", channel); + runSingleTest(channel, 5, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 6", channel); + runSingleTest(channel, 6, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 7", channel); + runSingleTest(channel, 7, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 8", channel); + runSingleTest(channel, 8, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + expected = 4; + writeTestMessage("distance 9", channel); + runSingleTest(channel, 9, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 10", channel); + runSingleTest(channel, 10, expected, xp); + + performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + "SingleKillManualCast.txt")); + } + + private void runSingleTest(ByteArrayDataOutput channel, int distance, int expected, int xp) + throws Exception, IOException + { + final Player player = pluginMockSetup(); + // Ensure we're using correct magic + when(player.getAnimation()).thenReturn(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF.animationId); + + final var it = iceElementals.iterator(); + final NPC target = it.next(); + // Set up hit delay by setting distance + final var bounds = getWorldForRegionId(11669); + final var wp = new WorldPoint(bounds.minX + 128, bounds.minY + 128, 0); + when(target.getWorldLocation()).thenReturn(wp); + when(player.getWorldLocation()).thenReturn(wp.dx(distance)); + when(player.getInteracting()).thenReturn(target); + while (it.hasNext()) + { + final var splashed = it.next(); + // need to mock all the other elementals, ensure they are off screen: + when(splashed.getWorldLocation()).thenReturn(wp.dx(128)); + } + + // Ensure we deal enough damage + underTest.onFakeXpDrop(new FakeXpDrop(Skill.HITPOINTS, xp)); + + onGameTick(channel); + + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + assertSame(expected, underTest.attackDelayHoldoffTicks); + } + + @Test + public void AoEKillManualCast() throws Exception + { + // We start quicker than the single kills and expect decay as they get further away due to hit delay + int expected = 1; + final int xp = 60; + + ByteArrayDataOutput channel = ByteStreams.newDataOutput(); + underTest.writeState(channel); + + writeTestMessage("distance 1", channel); + runAoETest(channel, 1, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 2", channel); + runAoETest(channel, 2, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + expected = 2; + writeTestMessage("distance 3", channel); + runAoETest(channel, 3, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 4", channel); + runAoETest(channel, 4, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 5", channel); + runAoETest(channel, 5, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + expected = 3; + writeTestMessage("distance 6", channel); + runAoETest(channel, 6, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 7", channel); + runAoETest(channel, 7, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 8", channel); + runAoETest(channel, 8, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + expected = 4; + writeTestMessage("distance 9", channel); + runAoETest(channel, 9, expected, xp); + for (int i = 0; i < expected; i++) + { + onGameTick(channel); + } + writeTestMessage("distance 10", channel); + runAoETest(channel, 10, expected, xp); + + performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + "AoEKillManualCast.txt")); + } + + private void runAoETest(ByteArrayDataOutput channel, int distance, int expected, int xp) + throws Exception, IOException + { + final Player player = pluginMockSetup(); + // Ensure we're using correct magic + when(player.getAnimation()).thenReturn(AnimationData.MAGIC_STANDARD_STRIKE_BOLT_BLAST_STAFF.animationId); + + when(mockedClient.getLocalPlayer()).thenReturn(player); + final var it = iceElementals.iterator(); + final NPC target = it.next(); + // Set up hit delay by setting distance + final var bounds = getWorldForRegionId(11669); + final var wp = new WorldPoint(bounds.minX + 128, bounds.minY + 128, 0); + when(target.getWorldLocation()).thenReturn(wp); + when(player.getWorldLocation()).thenReturn(wp.dx(distance)); + when(player.getInteracting()).thenReturn(target); + while (it.hasNext()) + { + final var splashed = it.next(); + when(splashed.getWorldLocation()).thenReturn(wp); + } + + // Ensure we deal enough damage + underTest.onFakeXpDrop(new FakeXpDrop(Skill.HITPOINTS, xp)); + + onGameTick(channel); + + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + assertSame(expected, underTest.attackDelayHoldoffTicks); + + // clean up: + for (final var elemental : iceElementals) + { + underTest.onNpcDespawned(new NpcDespawned(elemental)); + } + } + + @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); + + // need some extra mocks to stop the plugin running into an exception on the + // client APIs + // -- Mock World + + WorldView mockedWorldView = mock(WorldView.class); + when(mockedWorldView.isInstance()).thenReturn(true); + when(mockedWorldView.getInstanceTemplateChunks()).thenReturn(createInstanceTemplateChunks(11669)); + int mockedPlane = 0; + LocalPoint localPoint = LocalPoint.fromScene(30, 30, mockedWorldView); + when(mockedClient.getLocalPlayer()).thenReturn(mockedPlayer); + when(mockedPlayer.getLocalLocation()).thenReturn(localPoint); + when(mockedClient.getTopLevelWorldView()).thenReturn(mockedWorldView); + when(mockedClient.getWorldView(0)).thenReturn(mockedWorldView); + when(mockedWorldView.getPlane()).thenReturn(mockedPlane); + + // Finally turn the plugin "on" + underTest.startUp(); + + // Create the elementals + for (int i = 0; i < 3; i++) + { + final var ice = mock(NPC.class); + iceElementals.add(ice); + setNPCMock(ice, 14151); + underTest.onNpcSpawned(new NpcSpawned(ice)); + } + + return mockedPlayer; + } + + protected Set iceElementals = new HashSet(); + protected Set fireElementals = new HashSet(); +} diff --git a/src/test/java/com/attacktimer/TormentedDemonsTest.java b/src/test/java/com/attacktimer/TormentedDemonsTest.java index 7daff31..ad6f915 100644 --- a/src/test/java/com/attacktimer/TormentedDemonsTest.java +++ b/src/test/java/com/attacktimer/TormentedDemonsTest.java @@ -1,144 +1,144 @@ -package com.attacktimer; - -/* - * Copyright (c) 2026, Lexer747 - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import com.attacktimer.AttackTimerMetronomePlugin.AttackState; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import java.nio.file.Paths; -import net.runelite.api.EnumComposition; -import net.runelite.api.EnumID; -import net.runelite.api.NPC; -import net.runelite.api.Player; -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.events.NpcSpawned; -import net.runelite.client.game.ItemEquipmentStats; -import net.runelite.client.game.ItemStats; -import org.junit.Test; - -public class TormentedDemonsTest extends IntegrationTests -{ - @Test - public void PunishTest() throws Exception - { - // Equip a weapon which is normally MORE than 4 ticks and correct attack style - runTest("PunishTest", 8, 0, 0, 3); - } - - @Test - public void PunishWastedTest() throws Exception - { - runTest("PunishWastedTest", 4, 0, 0, 3); - } - - @Test - public void PunishWastedWrongStyleTest() throws Exception - { - runTest("PunishWastedWrongStyleTest", 8, 0, 8, 7); - } - - private void runTest(String testName, int aspeed, int EQUIPPED_WEAPON_TYPE, int ATTACK_STYLE, int expected) - throws Exception - { - when(mockedItemManager.getItemStats(-1)) - .thenReturn(new ItemStats(true, 0, 0, ItemEquipmentStats.builder().aspeed(aspeed).build())); - // Use weapon style specified by the test - when(mockedClient.getVarbitValue(Varbits.EQUIPPED_WEAPON_TYPE)).thenReturn(EQUIPPED_WEAPON_TYPE); - when(mockedClient.getVarpValue(VarPlayer.ATTACK_STYLE)).thenReturn(ATTACK_STYLE); - // -- Attack Styles - EnumComposition mockedWeaponEnum = mock(EnumComposition.class); - when(mockedClient.getEnum(EnumID.WEAPON_STYLES)).thenReturn(mockedWeaponEnum); - when(mockedWeaponEnum.getIntValue(0)).thenReturn(-1); - - Player player = pluginMockSetup(); - - ByteArrayDataOutput channel = ByteStreams.newDataOutput(); - underTest.writeState(channel); - - // Trigger the td spot animation so the next punish attack should be 4 ticks - when(td.hasSpotAnim(2852)).thenReturn(true); - // Tell the client a tick has occurred - onGameTick(channel); - assertSame(AttackState.NOT_ATTACKING, underTest.attackState); - // reset the animation (it does last longer than a tick but a tick should be all the client needs) - when(td.hasSpotAnim(2852)).thenReturn(false); - // finally "do an attack" and see if the plugin correctly noticed that this is a punish - when(player.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); - onGameTick(channel); - - assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); - assertSame(expected, underTest.attackDelayHoldoffTicks); - - performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + testName + ".txt")); - } - - @Override - public Player pluginMockSetup() throws Exception - { - // enable the plugin - when(mockedConfig.enableMetronome()).thenReturn(true); - // Create player - Player mockedPlayer = mock(Player.class); - when(mockedPlayer.getAnimation()).thenReturn(-1); - - // Create the tormented demon - td = mock(NPC.class); - int mockedNpcId = 13600; - setNPCMock(td, mockedNpcId); - - // set the player as "attacking" the NPC - when(mockedClient.getLocalPlayer()).thenReturn(mockedPlayer); - when(mockedPlayer.getInteracting()).thenReturn(td); - - // need some extra mocks to stop the plugin running into an exception on the - // client APIs - // -- Mock World - WorldView mockedWorldView = mock(WorldView.class); - int mockedPlane = 0; - LocalPoint localPoint = new LocalPoint(0, 0, mockedPlane); - when(mockedPlayer.getLocalLocation()).thenReturn(localPoint); - when(mockedClient.getTopLevelWorldView()).thenReturn(mockedWorldView); - when(mockedClient.getWorldView(0)).thenReturn(mockedWorldView); - when(mockedWorldView.getPlane()).thenReturn(mockedPlane); - // -- NPCs - - // Finally turn the plugin "on" - underTest.startUp(); - - // Only spawn the demon once the plugin is on. - underTest.onNpcSpawned(new NpcSpawned(td)); - return mockedPlayer; - } - - protected NPC td; -} +package com.attacktimer; + +/* + * Copyright (c) 2026, Lexer747 + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.attacktimer.AttackTimerMetronomePlugin.AttackState; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import java.nio.file.Paths; +import net.runelite.api.EnumComposition; +import net.runelite.api.EnumID; +import net.runelite.api.NPC; +import net.runelite.api.Player; +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.events.NpcSpawned; +import net.runelite.client.game.ItemEquipmentStats; +import net.runelite.client.game.ItemStats; +import org.junit.Test; + +public class TormentedDemonsTest extends IntegrationTests +{ + @Test + public void PunishTest() throws Exception + { + // Equip a weapon which is normally MORE than 4 ticks and correct attack style + runTest("PunishTest", 8, 0, 0, 3); + } + + @Test + public void PunishWastedTest() throws Exception + { + runTest("PunishWastedTest", 4, 0, 0, 3); + } + + @Test + public void PunishWastedWrongStyleTest() throws Exception + { + runTest("PunishWastedWrongStyleTest", 8, 0, 8, 7); + } + + private void runTest(String testName, int aspeed, int EQUIPPED_WEAPON_TYPE, int ATTACK_STYLE, int expected) + throws Exception + { + when(mockedItemManager.getItemStats(-1)) + .thenReturn(new ItemStats(true, 0, 0, ItemEquipmentStats.builder().aspeed(aspeed).build())); + // Use weapon style specified by the test + when(mockedClient.getVarbitValue(Varbits.EQUIPPED_WEAPON_TYPE)).thenReturn(EQUIPPED_WEAPON_TYPE); + when(mockedClient.getVarpValue(VarPlayer.ATTACK_STYLE)).thenReturn(ATTACK_STYLE); + // -- Attack Styles + EnumComposition mockedWeaponEnum = mock(EnumComposition.class); + when(mockedClient.getEnum(EnumID.WEAPON_STYLES)).thenReturn(mockedWeaponEnum); + when(mockedWeaponEnum.getIntValue(0)).thenReturn(-1); + + Player player = pluginMockSetup(); + + ByteArrayDataOutput channel = ByteStreams.newDataOutput(); + underTest.writeState(channel); + + // Trigger the td spot animation so the next punish attack should be 4 ticks + when(td.hasSpotAnim(2852)).thenReturn(true); + // Tell the client a tick has occurred + onGameTick(channel); + assertSame(AttackState.NOT_ATTACKING, underTest.attackState); + // reset the animation (it does last longer than a tick but a tick should be all the client needs) + when(td.hasSpotAnim(2852)).thenReturn(false); + // finally "do an attack" and see if the plugin correctly noticed that this is a punish + when(player.getAnimation()).thenReturn(AnimationData.MELEE_GENERIC_SLASH.animationId); + onGameTick(channel); + + assertSame(AttackState.DELAYED_FIRST_TICK, underTest.attackState); + assertSame(expected, underTest.attackDelayHoldoffTicks); + + performStateVerificationOrUpdate(channel, Paths.get(TESTDATA + testName + ".txt")); + } + + @Override + public Player pluginMockSetup() throws Exception + { + // enable the plugin + when(mockedConfig.enableMetronome()).thenReturn(true); + // Create player + Player mockedPlayer = mock(Player.class); + when(mockedPlayer.getAnimation()).thenReturn(-1); + + // Create the tormented demon + td = mock(NPC.class); + int mockedNpcId = 13600; + setNPCMock(td, mockedNpcId); + + // set the player as "attacking" the NPC + when(mockedClient.getLocalPlayer()).thenReturn(mockedPlayer); + when(mockedPlayer.getInteracting()).thenReturn(td); + + // need some extra mocks to stop the plugin running into an exception on the + // client APIs + // -- Mock World + WorldView mockedWorldView = mock(WorldView.class); + int mockedPlane = 0; + LocalPoint localPoint = new LocalPoint(0, 0, mockedPlane); + when(mockedPlayer.getLocalLocation()).thenReturn(localPoint); + when(mockedClient.getTopLevelWorldView()).thenReturn(mockedWorldView); + when(mockedClient.getWorldView(0)).thenReturn(mockedWorldView); + when(mockedWorldView.getPlane()).thenReturn(mockedPlane); + // -- NPCs + + // Finally turn the plugin "on" + underTest.startUp(); + + // Only spawn the demon once the plugin is on. + underTest.onNpcSpawned(new NpcSpawned(td)); + return mockedPlayer; + } + + protected NPC td; +} diff --git a/src/test/java/com/attacktimer/Update.java b/src/test/java/com/attacktimer/Update.java index 2e1e70f..e352d31 100644 --- a/src/test/java/com/attacktimer/Update.java +++ b/src/test/java/com/attacktimer/Update.java @@ -1,72 +1,72 @@ -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.google.common.collect.ImmutableMap; -import javax.annotation.Nonnull; - -public enum Update -{ - ALL("all"), - NONE("none"); - - @Nonnull - private final String value; - - Update(@Nonnull String value) - { - this.value = value; - } - - private static final ImmutableMap UPDATES; - - static - { - ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); - - for (Update data : values()) - { - builder.put(data.value, data); - } - - UPDATES = builder.build(); - } - - public static Update of(String value) - { - if (value == null) - return NONE; - Update update = UPDATES.get(value.toLowerCase()); - if (update == null) - return NONE; - return update; - } - - public static Update system() - { - return Update.of(System.getProperty("update")); - } -} +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.google.common.collect.ImmutableMap; +import javax.annotation.Nonnull; + +public enum Update +{ + ALL("all"), + NONE("none"); + + @Nonnull + private final String value; + + Update(@Nonnull String value) + { + this.value = value; + } + + private static final ImmutableMap UPDATES; + + static + { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + + for (Update data : values()) + { + builder.put(data.value, data); + } + + UPDATES = builder.build(); + } + + public static Update of(String value) + { + if (value == null) + return NONE; + Update update = UPDATES.get(value.toLowerCase()); + if (update == null) + return NONE; + return update; + } + + public static Update system() + { + return Update.of(System.getProperty("update")); + } +}