diff --git a/runelite-api/src/main/java/net/runelite/api/AnimationID.java b/runelite-api/src/main/java/net/runelite/api/AnimationID.java index d387270973..bed8bc49cc 100644 --- a/runelite-api/src/main/java/net/runelite/api/AnimationID.java +++ b/runelite-api/src/main/java/net/runelite/api/AnimationID.java @@ -113,6 +113,12 @@ public final class AnimationID public static final int LOOKING_INTO = 832; public static final int DIG = 830; public static final int VENGEANCE_OTHER = 4411; + public static final int DEMONIC_GORILLA_MAGIC_ATTACK = 7225; + public static final int DEMONIC_GORILLA_MELEE_ATTACK = 7226; + public static final int DEMONIC_GORILLA_RANGED_ATTACK = 7227; + public static final int DEMONIC_GORILLA_AOE_ATTACK = 7228; + public static final int DEMONIC_GORILLA_PRAYER_SWITCH = 7228; + public static final int DEMONIC_GORILLA_DEFEND = 7224; // NPC animations public static final int TZTOK_JAD_MAGIC_ATTACK = 2656; diff --git a/runelite-api/src/main/java/net/runelite/api/NPCComposition.java b/runelite-api/src/main/java/net/runelite/api/NPCComposition.java index 54adb11495..3601e78030 100644 --- a/runelite-api/src/main/java/net/runelite/api/NPCComposition.java +++ b/runelite-api/src/main/java/net/runelite/api/NPCComposition.java @@ -41,10 +41,12 @@ public interface NPCComposition int getId(); int getCombatLevel(); - + int[] getConfigs(); NPCComposition transform(); int getSize(); + + int getOverheadIcon(); } diff --git a/runelite-api/src/main/java/net/runelite/api/Player.java b/runelite-api/src/main/java/net/runelite/api/Player.java index 6ea285b3d1..f0158e0047 100644 --- a/runelite-api/src/main/java/net/runelite/api/Player.java +++ b/runelite-api/src/main/java/net/runelite/api/Player.java @@ -40,4 +40,6 @@ public interface Player extends Actor boolean isClanMember(); boolean isFriend(); + + int getOverheadIcon(); } diff --git a/runelite-api/src/main/java/net/runelite/api/ProjectileID.java b/runelite-api/src/main/java/net/runelite/api/ProjectileID.java index 163629020c..9347d66453 100644 --- a/runelite-api/src/main/java/net/runelite/api/ProjectileID.java +++ b/runelite-api/src/main/java/net/runelite/api/ProjectileID.java @@ -60,6 +60,10 @@ public class ProjectileID public static final int WINTERTODT_SNOW_FALL_AOE = 501; + public static final int DEMONIC_GORILLA_RANGED = 1302; + public static final int DEMONIC_GORILLA_MAGIC = 1304; + public static final int DEMONIC_GORILLA_BOULDER = 856; + /** * missing: marble gargoyle, superior dark beast */ diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/DemonicGorilla.java b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/DemonicGorilla.java new file mode 100644 index 0000000000..52e4409a9a --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/DemonicGorilla.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2018, Woox + * 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. + */ +package net.runelite.client.plugins.demonicgorilla; + +import java.util.Arrays; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.Actor; +import net.runelite.api.NPC; +import net.runelite.api.NPCComposition; +import net.runelite.api.coords.WorldArea; + +public class DemonicGorilla +{ + static final int MAX_ATTACK_RANGE = 10; // Needs <= 10 tiles to reach target + static final int ATTACK_RATE = 5; // 5 ticks between each attack + static final int ATTACKS_PER_SWITCH = 3; // 3 unsuccessful attacks per style switch + + static final int PROJECTILE_MAGIC_SPEED = 8; // Travels 8 tiles per tick + static final int PROJECTILE_RANGED_SPEED = 6; // Travels 6 tiles per tick + static final int PROJECTILE_MAGIC_DELAY = 12; // Requires an extra 12 tiles + static final int PROJECTILE_RANGED_DELAY = 9; // Requires an extra 9 tiles + + public static final AttackStyle[] ALL_REGULAR_ATTACK_STYLES = + { + AttackStyle.MELEE, + AttackStyle.RANGED, + AttackStyle.MAGIC + }; + + enum AttackStyle + { + MAGIC, + RANGED, + MELEE, + BOULDER + } + + @Getter + private NPC npc; + + @Getter + @Setter + private List nextPosibleAttackStyles; + + @Getter + @Setter + private int attacksUntilSwitch; + + @Getter + @Setter + private int nextAttackTick; + + @Getter + @Setter + private int lastTickAnimation; + + @Getter + @Setter + private WorldArea lastWorldArea; + + @Getter + @Setter + private boolean initiatedCombat; + + @Getter + @Setter + private Actor lastTickInteracting; + + @Getter + @Setter + private boolean takenDamageRecently; + + @Getter + @Setter + private int recentProjectileId; + + @Getter + @Setter + private boolean changedPrayerThisTick; + + @Getter + @Setter + private boolean changedAttackStyleThisTick; + + @Getter + @Setter + private boolean changedAttackStyleLastTick; + + @Getter + @Setter + private int lastTickOverheadIcon; + + @Getter + @Setter + private int disabledMeleeMovementForTicks; + + public DemonicGorilla(NPC npc) + { + this.npc = npc; + this.nextPosibleAttackStyles = Arrays.asList(ALL_REGULAR_ATTACK_STYLES); + this.nextAttackTick = -100; + this.attacksUntilSwitch = ATTACKS_PER_SWITCH; + this.recentProjectileId = -1; + this.lastTickOverheadIcon = -1; + } + + public int getOverheadIcon() + { + NPCComposition composition = this.npc.getComposition(); + if (composition != null) + { + return composition.getOverheadIcon(); + } + return -1; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/DemonicGorillaOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/DemonicGorillaOverlay.java new file mode 100644 index 0000000000..bc23adf01f --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/DemonicGorillaOverlay.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2018, Woox + * 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. + */ +package net.runelite.client.plugins.demonicgorilla; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.geom.Arc2D; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; +import net.runelite.api.Client; +import net.runelite.api.Perspective; +import net.runelite.api.Player; +import net.runelite.api.Point; +import net.runelite.api.Skill; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.game.SkillIconManager; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; + +public class DemonicGorillaOverlay extends Overlay +{ + private static final Color COLOR_ICON_BACKGROUND = new Color(0, 0, 0, 128); + private static final Color COLOR_ICON_BORDER = new Color(0, 0, 0, 255); + private static final Color COLOR_ICON_BORDER_FILL = new Color(219, 175, 0, 255); + private static final int OVERLAY_ICON_DISTANCE = 50; + private static final int OVERLAY_ICON_MARGIN = 8; + + private Client client; + private DemonicGorillaPlugin plugin; + + @Inject + private SkillIconManager iconManager; + + @Inject + public DemonicGorillaOverlay(Client client, DemonicGorillaPlugin plugin) + { + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + this.client = client; + this.plugin = plugin; + } + + private BufferedImage getIcon(DemonicGorilla.AttackStyle attackStyle) + { + switch (attackStyle) + { + case MELEE: return iconManager.getSkillImage(Skill.ATTACK); + case RANGED: return iconManager.getSkillImage(Skill.RANGED); + case MAGIC: return iconManager.getSkillImage(Skill.MAGIC); + } + return null; + } + + @Override + public Dimension render(Graphics2D graphics) + { + Player player = client.getLocalPlayer(); + + for (DemonicGorilla gorilla : plugin.getGorillas().values()) + { + if (gorilla.getNpc().getInteracting() == null) + { + continue; + } + + LocalPoint lp = gorilla.getNpc().getLocalLocation(); + if (lp != null) + { + Point point = Perspective.worldToCanvas(client, lp.getX(), lp.getY(), client.getPlane(), + gorilla.getNpc().getLogicalHeight() + 16); + if (point != null) + { + List attackStyles = gorilla.getNextPosibleAttackStyles(); + List icons = new ArrayList<>(); + int totalWidth = (attackStyles.size() - 1) * OVERLAY_ICON_MARGIN; + for (DemonicGorilla.AttackStyle attackStyle : attackStyles) + { + BufferedImage icon = getIcon(attackStyle); + icons.add(icon); + totalWidth += icon.getWidth(); + } + + int bgPadding = 4; + int currentPosX = 0; + for (BufferedImage icon : icons) + { + graphics.setStroke(new BasicStroke(2)); + graphics.setColor(COLOR_ICON_BACKGROUND); + graphics.fillOval( + point.getX() - totalWidth / 2 + currentPosX - bgPadding, + point.getY() - icon.getHeight() / 2 - OVERLAY_ICON_DISTANCE - bgPadding, + icon.getWidth() + bgPadding * 2, + icon.getHeight() + bgPadding * 2); + + graphics.setColor(COLOR_ICON_BORDER); + graphics.drawOval( + point.getX() - totalWidth / 2 + currentPosX - bgPadding, + point.getY() - icon.getHeight() / 2 - OVERLAY_ICON_DISTANCE - bgPadding, + icon.getWidth() + bgPadding * 2, + icon.getHeight() + bgPadding * 2); + + graphics.drawImage( + icon, + point.getX() - totalWidth / 2 + currentPosX, + point.getY() - icon.getHeight() / 2 - OVERLAY_ICON_DISTANCE, + null); + + graphics.setColor(COLOR_ICON_BORDER_FILL); + Arc2D.Double arc = new Arc2D.Double( + point.getX() - totalWidth / 2 + currentPosX - bgPadding, + point.getY() - icon.getHeight() / 2 - OVERLAY_ICON_DISTANCE - bgPadding, + icon.getWidth() + bgPadding * 2, + icon.getHeight() + bgPadding * 2, + 90.0, + -360.0 * (DemonicGorilla.ATTACKS_PER_SWITCH - + gorilla.getAttacksUntilSwitch()) / DemonicGorilla.ATTACKS_PER_SWITCH, + Arc2D.OPEN); + graphics.draw(arc); + + currentPosX += icon.getWidth() + OVERLAY_ICON_MARGIN; + } + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/DemonicGorillaPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/DemonicGorillaPlugin.java new file mode 100644 index 0000000000..43c7f5cc03 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/DemonicGorillaPlugin.java @@ -0,0 +1,695 @@ +/* + * Copyright (c) 2018, Woox + * 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. + */ +package net.runelite.client.plugins.demonicgorilla; + +import com.google.common.eventbus.Subscribe; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.inject.Inject; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.AnimationID; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.Hitsplat; +import net.runelite.api.NPC; +import net.runelite.api.NpcID; +import net.runelite.api.Player; +import net.runelite.api.Projectile; +import net.runelite.api.ProjectileID; +import net.runelite.api.coords.WorldArea; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.HitsplatApplied; +import net.runelite.api.events.NpcDespawned; +import net.runelite.api.events.NpcSpawned; +import net.runelite.api.events.PlayerDespawned; +import net.runelite.api.events.PlayerSpawned; +import net.runelite.api.events.ProjectileMoved; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.Overlay; + +@PluginDescriptor( + name = "Demonic gorillas" +) +@Slf4j +public class DemonicGorillaPlugin extends Plugin +{ + static final int OVERHEAD_ICON_MELEE = 0; + static final int OVERHEAD_ICON_RANGED = 1; + static final int OVERHEAD_ICON_MAGIC = 2; + + @Inject + private Client client; + + @Inject + private DemonicGorillaOverlay overlay; + + @Getter + private Map gorillas; + + private int tickCounter; + + private List recentBoulders; + + private List pendingAttacks; + + private Map memorizedPlayers; + + @Override + protected void startUp() throws Exception + { + tickCounter = 0; + gorillas = new HashMap<>(); + recentBoulders = new ArrayList<>(); + pendingAttacks = new ArrayList<>(); + memorizedPlayers = new HashMap<>(); + } + + @Override + protected void shutDown() throws Exception + { + gorillas = null; + recentBoulders = null; + pendingAttacks = null; + memorizedPlayers = null; + } + + @Override + public Overlay getOverlay() + { + return overlay; + } + + private void clear() + { + recentBoulders.clear(); + pendingAttacks.clear(); + gorillas.clear(); + memorizedPlayers.clear(); + } + + private void resetPlayers() + { + memorizedPlayers.clear(); + for (Player player : client.getPlayers()) + { + memorizedPlayers.put(player, new MemorizedPlayer(player)); + } + } + + public static boolean isNpcGorilla(int npcId) + { + return npcId == NpcID.DEMONIC_GORILLA || + npcId == NpcID.DEMONIC_GORILLA_7145 || + npcId == NpcID.DEMONIC_GORILLA_7146 || + npcId == NpcID.DEMONIC_GORILLA_7147 || + npcId == NpcID.DEMONIC_GORILLA_7148 || + npcId == NpcID.DEMONIC_GORILLA_7149; + } + + private void checkGorillaAttackStyleSwitch(DemonicGorilla gorilla, + final DemonicGorilla.AttackStyle... protectedStyles) + { + if (gorilla.getAttacksUntilSwitch() <= 0 || + gorilla.getNextPosibleAttackStyles().size() == 0) + { + gorilla.setNextPosibleAttackStyles(Arrays + .stream(DemonicGorilla.ALL_REGULAR_ATTACK_STYLES) + .filter(x -> !Arrays.stream(protectedStyles).anyMatch(y -> x == y)) + .collect(Collectors.toList())); + gorilla.setAttacksUntilSwitch(DemonicGorilla.ATTACKS_PER_SWITCH); + gorilla.setChangedAttackStyleThisTick(true); + } + } + + private DemonicGorilla.AttackStyle getProtectedStyle(Player player) + { + if (player.getOverheadIcon() == OVERHEAD_ICON_MELEE) + { + return DemonicGorilla.AttackStyle.MELEE; + } + else if (player.getOverheadIcon() == OVERHEAD_ICON_RANGED) + { + return DemonicGorilla.AttackStyle.RANGED; + } + else if (player.getOverheadIcon() == OVERHEAD_ICON_MAGIC) + { + return DemonicGorilla.AttackStyle.MAGIC; + } + return null; + } + + private void onGorillaAttack(DemonicGorilla gorilla, final DemonicGorilla.AttackStyle attackStyle) + { + gorilla.setInitiatedCombat(true); + + Player target = (Player)gorilla.getNpc().getInteracting(); + + DemonicGorilla.AttackStyle protectedStyle = null; + if (target != null) + { + protectedStyle = getProtectedStyle(target); + } + boolean correctPrayer = + target == null || // If player is out of memory, assume prayer was correct + attackStyle == protectedStyle; + + if (attackStyle == DemonicGorilla.AttackStyle.BOULDER) + { + // The gorilla can't throw boulders when it's meleeing + gorilla.setNextPosibleAttackStyles(gorilla + .getNextPosibleAttackStyles() + .stream() + .filter(x -> x != DemonicGorilla.AttackStyle.MELEE) + .collect(Collectors.toList())); + } + else + { + if (correctPrayer) + { + gorilla.setAttacksUntilSwitch(gorilla.getAttacksUntilSwitch() - 1); + } + else + { + // We're not sure if the attack will hit a 0 or not, + // so we don't know if we should decrease the counter or not, + // so we keep track of the attack here until the damage splat + // has appeared on the player. + + int damagesOnTick = tickCounter; + if (attackStyle == DemonicGorilla.AttackStyle.MAGIC) + { + MemorizedPlayer mp = memorizedPlayers.get(target); + WorldArea lastPlayerArea = mp.getLastWorldArea(); + if (lastPlayerArea != null) + { + int dist = gorilla.getNpc().getWorldArea().distanceTo(lastPlayerArea); + damagesOnTick += (dist + DemonicGorilla.PROJECTILE_MAGIC_DELAY) / + DemonicGorilla.PROJECTILE_MAGIC_SPEED; + } + } + else if (attackStyle == DemonicGorilla.AttackStyle.RANGED) + { + MemorizedPlayer mp = memorizedPlayers.get(target); + WorldArea lastPlayerArea = mp.getLastWorldArea(); + if (lastPlayerArea != null) + { + int dist = gorilla.getNpc().getWorldArea().distanceTo(lastPlayerArea); + damagesOnTick += (dist + DemonicGorilla.PROJECTILE_RANGED_DELAY) / + DemonicGorilla.PROJECTILE_RANGED_SPEED; + } + } + pendingAttacks.add(new PendingGorillaAttack(gorilla, attackStyle, target, damagesOnTick)); + } + + gorilla.setNextPosibleAttackStyles(gorilla + .getNextPosibleAttackStyles() + .stream() + .filter(x -> x == attackStyle) + .collect(Collectors.toList())); + + if (gorilla.getNextPosibleAttackStyles().size() == 0) + { + // Sometimes the gorilla can switch attack style before it's supposed to + // if someone was fighting it earlier and then left, so we just + // reset the counter in that case. + + gorilla.setNextPosibleAttackStyles(Arrays + .stream(DemonicGorilla.ALL_REGULAR_ATTACK_STYLES) + .filter(x -> x == attackStyle) + .collect(Collectors.toList())); + gorilla.setAttacksUntilSwitch(DemonicGorilla.ATTACKS_PER_SWITCH - + (correctPrayer ? 1 : 0)); + } + } + + checkGorillaAttackStyleSwitch(gorilla, protectedStyle); + + gorilla.setNextAttackTick(tickCounter + DemonicGorilla.ATTACK_RATE); + } + + private void checkGorillaAttacks() + { + for (DemonicGorilla gorilla : gorillas.values()) + { + Player interacting = (Player)gorilla.getNpc().getInteracting(); + MemorizedPlayer mp = memorizedPlayers.get(interacting); + + if (gorilla.getLastTickInteracting() != null && interacting == null) + { + gorilla.setInitiatedCombat(false); + } + else if (mp != null && mp.getLastWorldArea() != null && + !gorilla.isInitiatedCombat() && + tickCounter < gorilla.getNextAttackTick() && + gorilla.getNpc().getWorldArea().isInMeleeDistance(mp.getLastWorldArea())) + { + gorilla.setInitiatedCombat(true); + gorilla.setNextAttackTick(tickCounter + 1); + } + + int animationId = gorilla.getNpc().getAnimation(); + + if (gorilla.isTakenDamageRecently() && + tickCounter >= gorilla.getNextAttackTick() + 4) + { + // The gorilla was flinched, so its next attack gets delayed + gorilla.setNextAttackTick(tickCounter + DemonicGorilla.ATTACK_RATE / 2); + gorilla.setInitiatedCombat(true); + + if (mp != null && mp.getLastWorldArea() != null && + !gorilla.getNpc().getWorldArea().isInMeleeDistance(mp.getLastWorldArea()) && + !gorilla.getNpc().getWorldArea().intersectsWith(mp.getLastWorldArea())) + { + // Gorillas stop meleeing when they get flinched + // and the target isn't in melee distance + gorilla.setNextPosibleAttackStyles(gorilla + .getNextPosibleAttackStyles() + .stream() + .filter(x -> x != DemonicGorilla.AttackStyle.MELEE) + .collect(Collectors.toList())); + checkGorillaAttackStyleSwitch(gorilla, DemonicGorilla.AttackStyle.MELEE, + getProtectedStyle(interacting)); + } + } + else if (animationId != gorilla.getLastTickAnimation()) + { + if (animationId == AnimationID.DEMONIC_GORILLA_MELEE_ATTACK) + { + onGorillaAttack(gorilla, DemonicGorilla.AttackStyle.MELEE); + } + else if (animationId == AnimationID.DEMONIC_GORILLA_MAGIC_ATTACK) + { + onGorillaAttack(gorilla, DemonicGorilla.AttackStyle.MAGIC); + } + else if (animationId == AnimationID.DEMONIC_GORILLA_RANGED_ATTACK) + { + onGorillaAttack(gorilla, DemonicGorilla.AttackStyle.RANGED); + } + else if (animationId == AnimationID.DEMONIC_GORILLA_AOE_ATTACK && interacting != null) + { + // Note that AoE animation is the same as prayer switch animation + // so we need to check if the prayer was switched or not. + // It also does this animation when it spawns, so + // we need the interacting != null check. + + if (gorilla.getOverheadIcon() == gorilla.getLastTickOverheadIcon()) + { + // Confirmed, the gorilla used the AoE attack + onGorillaAttack(gorilla, DemonicGorilla.AttackStyle.BOULDER); + } + else + { + if (tickCounter >= gorilla.getNextAttackTick()) + { + gorilla.setChangedPrayerThisTick(true); + + // This part is more complicated because the gorilla may have + // used an attack, but the prayer switch animation takes + // priority over normal attack animations. + + int projectileId = gorilla.getRecentProjectileId(); + if (projectileId == ProjectileID.DEMONIC_GORILLA_MAGIC) + { + onGorillaAttack(gorilla, DemonicGorilla.AttackStyle.MAGIC); + } + else if (projectileId == ProjectileID.DEMONIC_GORILLA_RANGED) + { + onGorillaAttack(gorilla, DemonicGorilla.AttackStyle.RANGED); + } + else if (mp != null) + { + WorldArea lastPlayerArea = mp.getLastWorldArea(); + if (lastPlayerArea != null && + interacting != null && recentBoulders.stream() + .anyMatch(x -> x.distanceTo(lastPlayerArea) == 0)) + { + // A boulder started falling on the gorillas target, + // so we assume it was the gorilla who shot it + onGorillaAttack(gorilla, DemonicGorilla.AttackStyle.BOULDER); + } + else if (mp.getRecentHitsplats().size() > 0) + { + // It wasn't any of the three other attacks, + // but the player took damage, so we assume + // it's a melee attack + onGorillaAttack(gorilla, DemonicGorilla.AttackStyle.MELEE); + } + } + } + + // The next attack tick is always delayed if the + // gorilla switched prayer + gorilla.setNextAttackTick(tickCounter + DemonicGorilla.ATTACK_RATE); + gorilla.setChangedPrayerThisTick(true); + } + } + } + + if (gorilla.getDisabledMeleeMovementForTicks() > 0) + { + gorilla.setDisabledMeleeMovementForTicks(gorilla.getDisabledMeleeMovementForTicks() - 1); + } + else if (gorilla.isInitiatedCombat() && + gorilla.getNpc().getInteracting() != null && + !gorilla.isChangedAttackStyleThisTick() && + gorilla.getNextPosibleAttackStyles().size() >= 2 && + gorilla.getNextPosibleAttackStyles().stream() + .anyMatch(x -> x == DemonicGorilla.AttackStyle.MELEE)) + { + // If melee is a possibility, we can check if the gorilla + // is or isn't moving toward the player to determine if + // it is actually attempting to melee or not. + // We only run this check if the gorilla is in combat + // because otherwise it attempts to travel to melee + // distance before attacking its target. + + if (mp != null && mp.getLastWorldArea() != null && gorilla.getLastWorldArea() != null) + { + WorldArea predictedNewArea = gorilla.getLastWorldArea().calculateNextTravellingPoint( + client, mp.getLastWorldArea(), true, x -> + { + // Gorillas can't normally walk through other gorillas + // or other players + final WorldArea area1 = new WorldArea(x, 1, 1); + return area1 != null && + !gorillas.values().stream().anyMatch(y -> + { + if (y == gorilla) + { + return false; + } + final WorldArea area2 = + y.getNpc().getIndex() < gorilla.getNpc().getIndex() ? + y.getNpc().getWorldArea() : y.getLastWorldArea(); + return area2 != null && area1.intersectsWith(area2); + }) && + !memorizedPlayers.values().stream().anyMatch(y -> + { + final WorldArea area2 = y.getLastWorldArea(); + return area2 != null && area1.intersectsWith(area2); + }); + + // There is a special case where if a player walked through + // a gorilla, or a player walked through another player, + // the tiles that were walked through becomes + // walkable, but I didn't feel like it's necessary to handle + // that special case as it should rarely happen. + }); + if (predictedNewArea != null) + { + int distance = gorilla.getNpc().getWorldArea().distanceTo(mp.getLastWorldArea()); + WorldPoint predictedMovement = predictedNewArea.toWorldPoint(); + if (distance <= DemonicGorilla.MAX_ATTACK_RANGE && + mp != null && + mp.getLastWorldArea().hasLineOfSightTo(client, gorilla.getLastWorldArea())) + { + if (predictedMovement.distanceTo(gorilla.getLastWorldArea().toWorldPoint()) != 0) + { + if (predictedMovement.distanceTo(gorilla.getNpc().getWorldLocation()) == 0) + { + gorilla.setNextPosibleAttackStyles(gorilla + .getNextPosibleAttackStyles() + .stream() + .filter(x -> x == DemonicGorilla.AttackStyle.MELEE) + .collect(Collectors.toList())); + } + else + { + gorilla.setNextPosibleAttackStyles(gorilla + .getNextPosibleAttackStyles() + .stream() + .filter(x -> x != DemonicGorilla.AttackStyle.MELEE) + .collect(Collectors.toList())); + } + } + else if (tickCounter >= gorilla.getNextAttackTick() && + gorilla.getRecentProjectileId() == -1 && + !recentBoulders.stream().anyMatch(x -> x.distanceTo(mp.getLastWorldArea()) == 0)) + { + gorilla.setNextPosibleAttackStyles(gorilla + .getNextPosibleAttackStyles() + .stream() + .filter(x -> x == DemonicGorilla.AttackStyle.MELEE) + .collect(Collectors.toList())); + } + } + } + } + } + + if (gorilla.isTakenDamageRecently()) + { + gorilla.setInitiatedCombat(true); + } + + if (gorilla.getOverheadIcon() != gorilla.getLastTickOverheadIcon()) + { + if (gorilla.isChangedAttackStyleLastTick() || + gorilla.isChangedAttackStyleThisTick()) + { + // Apparently if it changes attack style and changes + // prayer on the same tick or 1 tick apart, it won't + // be able to move for the next 2 ticks if it attempts + // to melee + gorilla.setDisabledMeleeMovementForTicks(2); + } + else + { + // If it didn't change attack style lately, + // it's only for the next 1 tick + gorilla.setDisabledMeleeMovementForTicks(1); + } + } + gorilla.setLastTickAnimation(gorilla.getNpc().getAnimation()); + gorilla.setLastWorldArea(gorilla.getNpc().getWorldArea()); + gorilla.setLastTickInteracting(gorilla.getNpc().getInteracting()); + gorilla.setTakenDamageRecently(false); + gorilla.setChangedPrayerThisTick(false); + gorilla.setChangedAttackStyleLastTick(gorilla.isChangedAttackStyleThisTick()); + gorilla.setChangedAttackStyleThisTick(false); + gorilla.setLastTickOverheadIcon(gorilla.getOverheadIcon()); + gorilla.setRecentProjectileId(-1); + } + } + + @Subscribe + public void onProjectile(ProjectileMoved event) + { + Projectile projectile = event.getProjectile(); + int projectileId = projectile.getId(); + if (projectileId != ProjectileID.DEMONIC_GORILLA_RANGED && + projectileId != ProjectileID.DEMONIC_GORILLA_MAGIC && + projectileId != ProjectileID.DEMONIC_GORILLA_BOULDER) + { + return; + } + + // The event fires once before the projectile starts moving, + // and we only want to check each projectile once + if (client.getGameCycle() >= projectile.getStartMovementCycle()) + { + return; + } + + if (projectileId == ProjectileID.DEMONIC_GORILLA_BOULDER) + { + recentBoulders.add(WorldPoint.fromLocal(client, event.getPosition())); + } + else if (projectileId == ProjectileID.DEMONIC_GORILLA_MAGIC || + projectileId == ProjectileID.DEMONIC_GORILLA_RANGED) + { + WorldPoint projectileSourcePosition = WorldPoint.fromLocal( + client, projectile.getX1(), projectile.getY1(), client.getPlane()); + for (DemonicGorilla gorilla : gorillas.values()) + { + if (gorilla.getNpc().getWorldLocation().distanceTo(projectileSourcePosition) == 0) + { + gorilla.setRecentProjectileId(projectile.getId()); + } + } + } + } + + private void checkPendingAttacks() + { + Iterator it = pendingAttacks.iterator(); + while (it.hasNext()) + { + PendingGorillaAttack attack = it.next(); + if (tickCounter >= attack.getFinishesOnTick()) + { + boolean shouldDecreaseCounter = false; + DemonicGorilla gorilla = attack.getAttacker(); + MemorizedPlayer target = memorizedPlayers.get(attack.getTarget()); + if (target == null) + { + // Player went out of memory, so assume the hit was a 0 + shouldDecreaseCounter = true; + } + else if (target.getRecentHitsplats().size() == 0) + { + // No hitsplats was applied. This may happen in some cases + // where the player was out of memory while the + // projectile was travelling. So we assume the hit was a 0. + shouldDecreaseCounter = true; + } + else if (target.getRecentHitsplats().stream() + .anyMatch(x -> x.getHitsplatType() == Hitsplat.HitsplatType.BLOCK)) + { + // A blue hitsplat appeared, so we assume the gorilla hit a 0 + shouldDecreaseCounter = true; + } + + if (shouldDecreaseCounter) + { + gorilla.setAttacksUntilSwitch(gorilla.getAttacksUntilSwitch() - 1); + checkGorillaAttackStyleSwitch(gorilla); + } + + it.remove(); + } + } + } + + private void updatePlayers() + { + for (MemorizedPlayer mp : memorizedPlayers.values()) + { + mp.setLastWorldArea(mp.getPlayer().getWorldArea()); + mp.getRecentHitsplats().clear(); + } + } + + @Subscribe + public void onHitsplat(HitsplatApplied event) + { + if (gorillas.isEmpty()) + { + return; + } + + if (event.getActor() instanceof Player) + { + Player player = (Player)event.getActor(); + MemorizedPlayer mp = memorizedPlayers.get(player); + if (mp != null) + { + mp.getRecentHitsplats().add(event.getHitsplat()); + } + } + else if (event.getActor() instanceof NPC) + { + DemonicGorilla gorilla = gorillas.get(event.getActor()); + Hitsplat.HitsplatType hitsplatType = event.getHitsplat().getHitsplatType(); + if (gorilla != null && (hitsplatType == Hitsplat.HitsplatType.BLOCK || + hitsplatType == Hitsplat.HitsplatType.DAMAGE)) + { + gorilla.setTakenDamageRecently(true); + } + } + } + + @Subscribe + public void onGameState(GameStateChanged event) + { + GameState gs = event.getGameState(); + if (gs == GameState.LOGGING_IN || + gs == GameState.CONNECTION_LOST || + gs == GameState.HOPPING) + { + clear(); + } + } + + @Subscribe + public void onPlayerSpawned(PlayerSpawned event) + { + if (gorillas.isEmpty()) + { + return; + } + + Player player = event.getPlayer(); + memorizedPlayers.put(player, new MemorizedPlayer(player)); + } + + @Subscribe + public void onPlayerDespawned(PlayerDespawned event) + { + if (gorillas.isEmpty()) + { + return; + } + + memorizedPlayers.remove(event.getPlayer()); + } + + @Subscribe + public void onNpcSpawned(NpcSpawned event) + { + NPC npc = event.getNpc(); + if (isNpcGorilla(npc.getId())) + { + if (gorillas.isEmpty()) + { + // Players are not kept track of when there are no gorillas in + // memory, so we need to add the players that were already in memory. + resetPlayers(); + } + + gorillas.put(npc, new DemonicGorilla(npc)); + } + } + + @Subscribe + public void onNpcDespawned(NpcDespawned event) + { + if (gorillas.remove(event.getNpc()) != null && gorillas.isEmpty()) + { + clear(); + } + } + + @Subscribe + public void onGameTick(GameTick event) + { + checkGorillaAttacks(); + checkPendingAttacks(); + updatePlayers(); + recentBoulders.clear(); + + tickCounter++; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/MemorizedPlayer.java b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/MemorizedPlayer.java new file mode 100644 index 0000000000..7dd0058cd6 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/MemorizedPlayer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018, Woox + * 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. + */ +package net.runelite.client.plugins.demonicgorilla; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.Hitsplat; +import net.runelite.api.Player; +import net.runelite.api.coords.WorldArea; + +public class MemorizedPlayer +{ + @Getter + private Player player; + + @Getter + @Setter + private WorldArea lastWorldArea; + + @Getter + private List recentHitsplats; + + public MemorizedPlayer(Player player) + { + this.player = player; + this.recentHitsplats = new ArrayList<>(); + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/PendingGorillaAttack.java b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/PendingGorillaAttack.java new file mode 100644 index 0000000000..6f3faff8d2 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/demonicgorilla/PendingGorillaAttack.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2018, Woox + * 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. + */ +package net.runelite.client.plugins.demonicgorilla; + +import lombok.Getter; +import net.runelite.api.Player; + +public class PendingGorillaAttack +{ + @Getter + private DemonicGorilla attacker; + + @Getter + private DemonicGorilla.AttackStyle attackStyle; + + @Getter + private Player target; + + @Getter + private int finishesOnTick; + + public PendingGorillaAttack(DemonicGorilla attacker, DemonicGorilla.AttackStyle attackStyle, + Player target, int finishesOnTick) + { + this.attacker = attacker; + this.attackStyle = attackStyle; + this.target = target; + this.finishesOnTick = finishesOnTick; + } +} \ No newline at end of file diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSActor.java b/runescape-api/src/main/java/net/runelite/rs/api/RSActor.java index 2c68afbbc6..45ba0ecb59 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSActor.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSActor.java @@ -92,4 +92,13 @@ public interface RSActor extends RSRenderable, Actor @Import("spotAnimFrameCycle") int getSpotAnimFrameCycle(); + + @Import("hitsplatValues") + int[] getHitsplatValues(); + + @Import("hitsplatTypes") + int[] getHitsplatTypes(); + + @Import("hitsplatCycles") + int[] getHitsplatCycles(); } diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSNPCComposition.java b/runescape-api/src/main/java/net/runelite/rs/api/RSNPCComposition.java index ed855b2557..dbfd50a291 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSNPCComposition.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSNPCComposition.java @@ -72,4 +72,8 @@ public interface RSNPCComposition extends NPCComposition @Import("size") @Override int getSize(); + + @Import("headIcon") + @Override + int getOverheadIcon(); } diff --git a/runescape-api/src/main/java/net/runelite/rs/api/RSPlayer.java b/runescape-api/src/main/java/net/runelite/rs/api/RSPlayer.java index 1e693a0e94..ddfa78c75a 100644 --- a/runescape-api/src/main/java/net/runelite/rs/api/RSPlayer.java +++ b/runescape-api/src/main/java/net/runelite/rs/api/RSPlayer.java @@ -57,4 +57,8 @@ public interface RSPlayer extends RSActor, Player @Import("isFriend") @Override boolean isFriend(); + + @Import("overheadIcon") + @Override + int getOverheadIcon(); }