Add demonic gorilla plugin

This commit is contained in:
WooxSolo
2018-04-28 11:35:52 -04:00
committed by Adam
parent 1966bba3c9
commit ef9c1ae0c5
12 changed files with 1126 additions and 1 deletions

View File

@@ -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;

View File

@@ -41,10 +41,12 @@ public interface NPCComposition
int getId();
int getCombatLevel();
int[] getConfigs();
NPCComposition transform();
int getSize();
int getOverheadIcon();
}

View File

@@ -40,4 +40,6 @@ public interface Player extends Actor
boolean isClanMember();
boolean isFriend();
int getOverheadIcon();
}

View File

@@ -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
*/

View File

@@ -0,0 +1,140 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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<AttackStyle> 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;
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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<DemonicGorilla.AttackStyle> attackStyles = gorilla.getNextPosibleAttackStyles();
List<BufferedImage> 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;
}
}

View File

@@ -0,0 +1,695 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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<NPC, DemonicGorilla> gorillas;
private int tickCounter;
private List<WorldPoint> recentBoulders;
private List<PendingGorillaAttack> pendingAttacks;
private Map<Player, MemorizedPlayer> 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<PendingGorillaAttack> 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++;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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<Hitsplat> recentHitsplats;
public MemorizedPlayer(Player player)
{
this.player = player;
this.recentHitsplats = new ArrayList<>();
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2018, Woox <https://github.com/wooxsolo>
* 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;
}
}

View File

@@ -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();
}

View File

@@ -72,4 +72,8 @@ public interface RSNPCComposition extends NPCComposition
@Import("size")
@Override
int getSize();
@Import("headIcon")
@Override
int getOverheadIcon();
}

View File

@@ -57,4 +57,8 @@ public interface RSPlayer extends RSActor, Player
@Import("isFriend")
@Override
boolean isFriend();
@Import("overheadIcon")
@Override
int getOverheadIcon();
}