diff --git a/alchemicalhydra/Hydra.java b/alchemicalhydra/Hydra.java new file mode 100644 index 0000000000..9544c95f11 --- /dev/null +++ b/alchemicalhydra/Hydra.java @@ -0,0 +1,62 @@ +package net.runelite.client.plugins.alchemicalhydra; + +import javax.inject.Singleton; +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.Prayer; +import net.runelite.api.ProjectileID; + +@Singleton +class Hydra +{ + enum AttackStyle + { + MAGIC(ProjectileID.HYDRA_MAGIC, Prayer.PROTECT_FROM_MAGIC), + RANGED(ProjectileID.HYDRA_RANGED, Prayer.PROTECT_FROM_MISSILES); + + @Getter + private int projId; + + @Getter + private Prayer prayer; + + AttackStyle(int projId, Prayer prayer) + { + this.projId = projId; + this.prayer = prayer; + } + } + + @Getter + @Setter + private HydraPhase phase; + + @Getter + @Setter + private int attackCount; + + @Getter + @Setter + private int nextSwitch; + + @Getter + @Setter + private int nextSpecial; + + @Getter + @Setter + private AttackStyle nextAttack; + + @Getter + @Setter + private AttackStyle lastAttack; + + Hydra() + { + this.phase = HydraPhase.ONE; + this.nextAttack = AttackStyle.MAGIC; + this.nextSpecial = 3; + this.nextSwitch = phase.getAttacksPerSwitch(); + this.attackCount = 0; + } +} diff --git a/alchemicalhydra/HydraOverlay.java b/alchemicalhydra/HydraOverlay.java new file mode 100644 index 0000000000..39f0f5fc97 --- /dev/null +++ b/alchemicalhydra/HydraOverlay.java @@ -0,0 +1,132 @@ +package net.runelite.client.plugins.alchemicalhydra; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.api.Client; +import net.runelite.api.Prayer; +import net.runelite.api.SpriteID; +import net.runelite.client.game.SpriteManager; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.InfoBoxComponent; +import net.runelite.client.ui.overlay.components.PanelComponent; + +@Singleton +class HydraOverlay extends Overlay +{ + private final HydraPlugin plugin; + private final Client client; + private final SpriteManager spriteManager; + private final PanelComponent panelComponent = new PanelComponent(); + private static final Color redBgCol = new Color(156, 0, 0, 156); + private static final Color yelBgCol = new Color(200, 156, 0, 156); + private static final Color grnBgCol = new Color(0, 156, 0, 156); + + @Inject + HydraOverlay(HydraPlugin plugin, Client client, SpriteManager spriteManager) + { + this.plugin = plugin; + this.client = client; + this.spriteManager = spriteManager; + setPosition(OverlayPosition.BOTTOM_RIGHT); + panelComponent.setOrientation(PanelComponent.Orientation.VERTICAL); + } + + @Override + public Dimension render(Graphics2D graphics2D) + { + Hydra hydra = plugin.getHydra(); + panelComponent.getChildren().clear(); + + if (hydra == null || client == null) + { + return null; + } + + //Add spec overlay first, to keep it above pray + HydraPhase phase = hydra.getPhase(); + int attackCount = hydra.getAttackCount(); + int nextSpec = hydra.getNextSpecial() - attackCount; + + if (nextSpec <= 3) + { + InfoBoxComponent specComponent = new InfoBoxComponent(); + + if (nextSpec == 0) + { + specComponent.setBackgroundColor(redBgCol); + } + else if (nextSpec == 1) + { + specComponent.setBackgroundColor(yelBgCol); + } + Image specImg = scaleImg(spriteManager.getSprite(phase.getSpecImage(), 0)); + + specComponent.setImage(specImg); + specComponent.setText(" " + (nextSpec)); //hacky way to not have to figure out how to move text + specComponent.setPreferredSize(new Dimension(40, 40)); + panelComponent.getChildren().add(specComponent); + } + + + Prayer nextPrayer = hydra.getNextAttack().getPrayer(); + Image prayImg = scaleImg(getPrayerImage(hydra.getNextAttack().getPrayer())); + int nextSwitch = hydra.getNextSwitch(); + + InfoBoxComponent prayComponent = new InfoBoxComponent(); + + if (nextSwitch == 1) + { + prayComponent.setBackgroundColor(client.isPrayerActive(nextPrayer) ? yelBgCol : redBgCol); + } + else + { + prayComponent.setBackgroundColor(client.isPrayerActive(nextPrayer) ? grnBgCol : redBgCol); + } + + prayComponent.setImage(prayImg); + prayComponent.setText(" " + nextSwitch); + prayComponent.setColor(Color.white); + prayComponent.setPreferredSize(new Dimension(40, 40)); + panelComponent.getChildren().add(prayComponent); + + panelComponent.setPreferredSize(new Dimension(40, 0)); + panelComponent.setBorder(new Rectangle(0, 0, 0, 0)); + return panelComponent.render(graphics2D); + } + + private BufferedImage getPrayerImage(Prayer pray) + { + return pray == Prayer.PROTECT_FROM_MAGIC + ? spriteManager.getSprite(SpriteID.PRAYER_PROTECT_FROM_MAGIC, 0) + : spriteManager.getSprite(SpriteID.PRAYER_PROTECT_FROM_MISSILES, 0); + } + + private Image scaleImg(final Image img) + { + if (img == null) + { + return null; + } + final double width = img.getWidth(null); + final double height = img.getHeight(null); + final double size = 36; // Limit size to 2 as that is minimum size not causing breakage + final double scalex = size / width; + final double scaley = size / height; + final double scale = Math.min(scalex, scaley); + final int newWidth = (int) (width * scale); + final int newHeight = (int) (height * scale); + final BufferedImage scaledImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); + final Graphics g = scaledImage.createGraphics(); + g.drawImage(img, 0, 0, newWidth, newHeight, null); + g.dispose(); + return scaledImage; + } +} diff --git a/alchemicalhydra/HydraPhase.java b/alchemicalhydra/HydraPhase.java new file mode 100644 index 0000000000..1764722d51 --- /dev/null +++ b/alchemicalhydra/HydraPhase.java @@ -0,0 +1,42 @@ +package net.runelite.client.plugins.alchemicalhydra; + +import lombok.Getter; +import net.runelite.api.AnimationID; +import net.runelite.api.ProjectileID; +import net.runelite.api.SpriteID; + +enum HydraPhase +{ + ONE(3, AnimationID.HYDRA_1_1, AnimationID.HYDRA_1_2, ProjectileID.HYDRA_POISON, 0, SpriteID.BIG_ASS_GUTHIX_SPELL), + TWO(3, AnimationID.HYDRA_2_1, AnimationID.HYDRA_2_2, 0, AnimationID.HYDRA_LIGHTNING, SpriteID.BIG_SPEC_TRANSFER), + THREE(3, AnimationID.HYDRA_3_1, AnimationID.HYDRA_3_2, 0, AnimationID.HYDRA_FIRE, SpriteID.BIG_SUPERHEAT), + FOUR(1, AnimationID.HYDRA_4_1, AnimationID.HYDRA_4_2, ProjectileID.HYDRA_POISON, 0, SpriteID.BIG_ASS_GUTHIX_SPELL); + + @Getter + private int attacksPerSwitch; + + @Getter + private int deathAnim1; + + @Getter + private int deathAnim2; + + @Getter + private int specProjectileId; + + @Getter + private int specAnimationId; + + @Getter + private int specImage; + + HydraPhase(int attacksPerSwitch, int deathAnim1, int deathAnim2, int specProjectileId, int specAnimationId, int specImage) + { + this.attacksPerSwitch = attacksPerSwitch; + this.deathAnim1 = deathAnim1; + this.deathAnim2 = deathAnim2; + this.specProjectileId = specProjectileId; + this.specAnimationId = specAnimationId; + this.specImage = specImage; + } +} \ No newline at end of file diff --git a/alchemicalhydra/HydraPlugin.java b/alchemicalhydra/HydraPlugin.java new file mode 100644 index 0000000000..da9e4f6c90 --- /dev/null +++ b/alchemicalhydra/HydraPlugin.java @@ -0,0 +1,248 @@ +package net.runelite.client.plugins.alchemicalhydra; + +import java.util.Arrays; +import java.util.HashSet; +import javax.inject.Inject; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.Actor; +import net.runelite.api.Client; +import net.runelite.api.GameState; +import net.runelite.api.NpcID; +import net.runelite.api.Projectile; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.events.AnimationChanged; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.NpcSpawned; +import net.runelite.api.events.ProjectileMoved; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.PluginType; +import net.runelite.client.ui.overlay.OverlayManager; + +@PluginDescriptor( + name = "Alchemical Hydra", + description = "Show what to pray against hydra", + tags = {"Hydra", "Lazy", "4 headed asshole"}, + type = PluginType.PVM +) +@Slf4j +public class HydraPlugin extends Plugin +{ + @Getter + private HashSet poisonPoints = new HashSet<>(); + + @Getter + private Hydra hydra; + + private boolean inHydraInstance; + private int lastAttackTick; + private int lastPoisonTick; + + private static final int[] HYDRA_REGIONS = { + 5279, 5280, + 5535, 5536 + }; + + @Inject + private Client client; + + @Inject + private OverlayManager overlayManager; + + @Inject + private HydraOverlay overlay; + + @Inject + private HydraPoisonOverlay poisonOverlay; + + @Override + protected void startUp() + { + inHydraInstance = checkArea(); + lastAttackTick = -1; + poisonPoints.clear(); + } + + @Override + protected void shutDown() + { + inHydraInstance = false; + hydra = null; + poisonPoints.clear(); + removeOverlays(); + lastAttackTick = -1; + } + + @Subscribe + private void onGameStateChanged(GameStateChanged state) + { + if (state.getGameState() != GameState.LOGGED_IN) + { + return; + } + + inHydraInstance = checkArea(); + + if (inHydraInstance) + { + hydra = new Hydra(); + log.debug("Entered hydra instance"); + addOverlays(); + } + else if (hydra != null) + { + removeOverlays(); + hydra = null; + log.debug("Left hydra instance"); + } + } + + @Subscribe + private void onNpcSpawned(NpcSpawned event) + { + if (!inHydraInstance || event.getNpc().getId() != NpcID.ALCHEMICAL_HYDRA) + { + return; + } + + hydra = new Hydra(); + log.debug("Hydra spawned"); + addOverlays(); + } + + @Subscribe + public void onAnimationChanged(AnimationChanged animationChanged) + { + Actor actor = animationChanged.getActor(); + + if (!inHydraInstance || hydra == null || actor == client.getLocalPlayer()) + { + return; + } + + HydraPhase phase = hydra.getPhase(); + + // Using the first animation sometimes fucks shit up, so just use 2 + if ( /* actor.getAnimation() == phase.getDeathAnim1() || */ actor.getAnimation() == phase.getDeathAnim2()) + { + switch (phase) + { + case ONE: + changePhase(HydraPhase.TWO); + log.debug("Hydra phase 2"); + return; + case TWO: + changePhase(HydraPhase.THREE); + log.debug("Hydra phase 3"); + return; + case THREE: + changePhase(HydraPhase.FOUR); + log.debug("Hydra phase 4"); + return; + case FOUR: + hydra = null; + poisonPoints.clear(); + log.debug("Hydra dead"); + removeOverlays(); + return; + default: + log.debug("Tried some weird shit"); + break; + } + + if (actor.getAnimation() == phase.getDeathAnim1() && phase == HydraPhase.THREE) + { + changePhase(HydraPhase.FOUR); + } + } + else if (actor.getAnimation() == phase.getSpecAnimationId() && phase.getSpecAnimationId() != 0) + { + hydra.setNextSpecial(hydra.getNextSpecial() + 9); + } + + if (!poisonPoints.isEmpty() && lastPoisonTick + 10 < client.getTickCount()) + { + poisonPoints.clear(); + } + } + + @Subscribe + public void onProjectileMoved(ProjectileMoved event) + { + if (!inHydraInstance || hydra == null + || client.getGameCycle() >= event.getProjectile().getStartMovementCycle()) + { + return; + } + + Projectile projectile = event.getProjectile(); + int id = projectile.getId(); + if (hydra.getPhase().getSpecProjectileId() != 0 && hydra.getPhase().getSpecProjectileId() == id) + { + poisonPoints.add(event.getPosition()); + hydra.setNextSpecial(hydra.getNextSpecial() + 9); + lastPoisonTick = client.getTickCount(); + } + else if (client.getTickCount() != lastAttackTick + && (id == Hydra.AttackStyle.MAGIC.getProjId() || id == Hydra.AttackStyle.RANGED.getProjId())) + { + handleAttack(id); + lastAttackTick = client.getTickCount(); + } + } + + private boolean checkArea() + { + return Arrays.equals(client.getMapRegions(), HYDRA_REGIONS) && client.isInInstancedRegion(); + } + + private void addOverlays() + { + overlayManager.add(overlay); + overlayManager.add(poisonOverlay); + } + + private void removeOverlays() + { + overlayManager.remove(overlay); + overlayManager.remove(poisonOverlay); + } + + private void changePhase(HydraPhase newPhase) + { + hydra.setPhase(newPhase); + hydra.setNextSpecial(3); + hydra.setAttackCount(0); + if (newPhase == HydraPhase.FOUR) + { + switchStyles(); + hydra.setNextSwitch(newPhase.getAttacksPerSwitch()); + } + } + + private void switchStyles() + { + hydra.setNextAttack(hydra.getLastAttack() == Hydra.AttackStyle.MAGIC + ? Hydra.AttackStyle.RANGED + : Hydra.AttackStyle.MAGIC); + } + + private void handleAttack(int id) + { + hydra.setNextSwitch(hydra.getNextSwitch() - 1); + hydra.setAttackCount(hydra.getAttackCount() + 1); + hydra.setLastAttack(hydra.getNextAttack()); + + if (id != hydra.getNextAttack().getProjId()) + { + switchStyles(); + } + else if (hydra.getNextSwitch() <= 0) + { + switchStyles(); + hydra.setNextSwitch(hydra.getPhase().getAttacksPerSwitch()); + } + } +} diff --git a/alchemicalhydra/HydraPoisonOverlay.java b/alchemicalhydra/HydraPoisonOverlay.java new file mode 100644 index 0000000000..c947e30051 --- /dev/null +++ b/alchemicalhydra/HydraPoisonOverlay.java @@ -0,0 +1,61 @@ +package net.runelite.client.plugins.alchemicalhydra; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.geom.Area; +import java.util.HashSet; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.api.Client; +import static net.runelite.api.Perspective.getCanvasTileAreaPoly; +import net.runelite.api.coords.LocalPoint; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayPosition; + +@Singleton +class HydraPoisonOverlay extends Overlay +{ + private final HydraPlugin plugin; + private final Client client; + + @Inject + public HydraPoisonOverlay(Client client, HydraPlugin plugin) + { + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.UNDER_WIDGETS); + this.plugin = plugin; + this.client = client; + } + + @Override + public Dimension render(Graphics2D graphics) + { + final HashSet initialPoints = plugin.getPoisonPoints(); + Area poisonTiles = new Area(); + for (LocalPoint point : initialPoints) + { + Polygon poly = getCanvasTileAreaPoly(client, point, 3); + if (poly == null) + { + break; + } + + poisonTiles.add(new Area(poly)); + } + + if (poisonTiles.isEmpty()) + { + return null; + } + graphics.setPaintMode(); + graphics.setColor(new Color(255, 0, 0, 75)); + graphics.draw(poisonTiles); + graphics.setColor(new Color(255, 0, 0, 30)); + graphics.fill(poisonTiles); + + return null; + } +} \ No newline at end of file