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