From a2588f82025bea8e14ef53924cd9ae81b36f1226 Mon Sep 17 00:00:00 2001 From: KowabungaCheese Date: Fri, 12 Jun 2026 20:04:49 -0500 Subject: [PATCH 1/2] zulrah-helper: add melee rotation overlay with stand positions and movement arrows --- .gitignore | 3 +- .../com/zulrahhelper/ZulrahHelperConfig.java | 20 +++- .../com/zulrahhelper/ZulrahHelperPlugin.java | 2 + .../zulrahhelper/options/ArrowDirection.java | 105 ++++++++++++++++++ .../options/MeleeStandLocation.java | 73 ++++++++++++ .../com/zulrahhelper/options/MeleeStep.java | 46 ++++++++ .../com/zulrahhelper/tree/PatternTree.java | 63 +++++++++++ src/main/java/com/zulrahhelper/tree/Step.java | 4 + src/main/java/com/zulrahhelper/ui/Images.java | 74 ++++++------ .../com/zulrahhelper/ImageBuilderTest.java | 77 +++++++++++++ 10 files changed, 431 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/zulrahhelper/options/ArrowDirection.java create mode 100644 src/main/java/com/zulrahhelper/options/MeleeStandLocation.java create mode 100644 src/main/java/com/zulrahhelper/options/MeleeStep.java diff --git a/.gitignore b/.gitignore index a23846baf..e396a94fa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ nbactions.xml nb-configuration.xml nbproject/ .DS_Store -out/ \ No newline at end of file +out/ +bin diff --git a/src/main/java/com/zulrahhelper/ZulrahHelperConfig.java b/src/main/java/com/zulrahhelper/ZulrahHelperConfig.java index 2bf16484c..73cff4005 100644 --- a/src/main/java/com/zulrahhelper/ZulrahHelperConfig.java +++ b/src/main/java/com/zulrahhelper/ZulrahHelperConfig.java @@ -31,6 +31,18 @@ public interface ZulrahHelperConfig extends Config ) String SECTION_MISC = ZulrahHelperPlugin.SECTION_MISC; + @ConfigItem( + keyName = ZulrahHelperPlugin.USING_MELEE_KEY, + section = ZulrahHelperPlugin.SECTION_IMAGE_OPTIONS, + name = "Using Melee", + description = "Show melee-optimized stand positions (closer to Zulrah spawn) instead of ranged positions", + position = 0 + ) + default boolean usingMelee() + { + return false; + } + @ConfigItem( keyName = ZulrahHelperPlugin.DISPLAY_PRAYER_KEY, section = ZulrahHelperPlugin.SECTION_IMAGE_OPTIONS, @@ -38,7 +50,7 @@ public interface ZulrahHelperConfig extends Config description = "Set phase images to use prayer icons, " + "denoting what overhead prayer to use per phase. " + "No prayer icon means the phase is safe to turn overheads off.", - position = 0 + position = 1 ) default boolean displayPrayerIcons() { @@ -50,7 +62,7 @@ default boolean displayPrayerIcons() section = ZulrahHelperPlugin.SECTION_IMAGE_OPTIONS, name = "Attack Icons", description = "Display number of Zulrah attacks", - position = 1 + position = 2 ) default boolean displayAttackIcons() { @@ -62,7 +74,7 @@ default boolean displayAttackIcons() section = ZulrahHelperPlugin.SECTION_IMAGE_OPTIONS, name = "Venom Icons", description = "Display number of venom attacks", - position = 1 + position = 2 ) default boolean displayVenom() { @@ -74,7 +86,7 @@ default boolean displayVenom() section = ZulrahHelperPlugin.SECTION_IMAGE_OPTIONS, name = "Snakeling Icons", description = "Display snakeling spawns", - position = 1 + position = 2 ) default boolean displaySnakelings() { diff --git a/src/main/java/com/zulrahhelper/ZulrahHelperPlugin.java b/src/main/java/com/zulrahhelper/ZulrahHelperPlugin.java index 49b002a05..d8c3026bc 100644 --- a/src/main/java/com/zulrahhelper/ZulrahHelperPlugin.java +++ b/src/main/java/com/zulrahhelper/ZulrahHelperPlugin.java @@ -39,6 +39,7 @@ public class ZulrahHelperPlugin extends Plugin static final String SECTION_HOTKEYS = "Hotkeys"; static final String SECTION_MISC = "Miscellaneous"; + static final String USING_MELEE_KEY = "usingMelee"; static final String DARK_MODE_KEY = "darkMode"; static final String DISPLAY_PRAYER_KEY = "displayPrayer"; static final String DISPLAY_ATTACK_KEY = "displayAttack"; @@ -57,6 +58,7 @@ public class ZulrahHelperPlugin extends Plugin private static final int ZULRAH_REGION_ID = 9008; private static final List OPTION_KEYS = Arrays.asList( + USING_MELEE_KEY, DARK_MODE_KEY, DISPLAY_PRAYER_KEY, DISPLAY_ATTACK_KEY, diff --git a/src/main/java/com/zulrahhelper/options/ArrowDirection.java b/src/main/java/com/zulrahhelper/options/ArrowDirection.java new file mode 100644 index 000000000..cea4126ce --- /dev/null +++ b/src/main/java/com/zulrahhelper/options/ArrowDirection.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024, Ron Young + * 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 com.zulrahhelper.options; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.geom.QuadCurve2D; + +public enum ArrowDirection +{ + NONE, + UP, + DOWN, + LEFT, + RIGHT, + UP_LEFT, + UP_RIGHT, + DOWN_LEFT, + DOWN_RIGHT; + + private static final int ARROWHEAD_SIZE = 5; + private static final double ARROWHEAD_SPREAD = Math.toRadians(60); + private static final int TIP_GAP = 6; + private static final int ARC_CONTROL_SHIFT = 6; + + public void drawArrow(Graphics2D g, int x1, int y1, int x2, int y2) + { + if (this == NONE) + { + return; + } + + g.setColor(Color.WHITE); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + if (!isDiagonal()) + { + double angle = Math.atan2(y2 - y1, x2 - x1); + int tipX = (int) Math.round(x2 - TIP_GAP * Math.cos(angle)); + int tipY = (int) Math.round(y2 - TIP_GAP * Math.sin(angle)); + g.drawLine(x1, y1, tipX, tipY); + drawArrowhead(g, tipX, tipY, angle); + } + else + { + // Bow the curve perpendicular to the travel direction (CW = right of travel) + double dx = x2 - x1; + double dy = y2 - y1; + double len = Math.sqrt(dx * dx + dy * dy); + double perpX = dy / len; + double perpY = -dx / len; + + int cx = (int) Math.round((x1 + x2) / 2.0 + perpX * ARC_CONTROL_SHIFT); + int cy = (int) Math.round((y1 + y2) / 2.0 + perpY * ARC_CONTROL_SHIFT); + + // Tangent at the end of the bezier: direction from control point to endpoint + double tangentAngle = Math.atan2(y2 - cy, x2 - cx); + int tipX = (int) Math.round(x2 - TIP_GAP * Math.cos(tangentAngle)); + int tipY = (int) Math.round(y2 - TIP_GAP * Math.sin(tangentAngle)); + + g.setStroke(new BasicStroke(1)); + g.draw(new QuadCurve2D.Float(x1, y1, cx, cy, tipX, tipY)); + drawArrowhead(g, tipX, tipY, tangentAngle); + } + } + + private boolean isDiagonal() + { + return this == UP_LEFT || this == UP_RIGHT || this == DOWN_LEFT || this == DOWN_RIGHT; + } + + private void drawArrowhead(Graphics2D g, int tipX, int tipY, double angle) + { + int bx1 = (int) Math.round(tipX - ARROWHEAD_SIZE * Math.cos(angle - ARROWHEAD_SPREAD / 2)); + int by1 = (int) Math.round(tipY - ARROWHEAD_SIZE * Math.sin(angle - ARROWHEAD_SPREAD / 2)); + int bx2 = (int) Math.round(tipX - ARROWHEAD_SIZE * Math.cos(angle + ARROWHEAD_SPREAD / 2)); + int by2 = (int) Math.round(tipY - ARROWHEAD_SIZE * Math.sin(angle + ARROWHEAD_SPREAD / 2)); + g.fillPolygon(new int[]{tipX, bx1, bx2}, new int[]{tipY, by1, by2}, 3); + } +} diff --git a/src/main/java/com/zulrahhelper/options/MeleeStandLocation.java b/src/main/java/com/zulrahhelper/options/MeleeStandLocation.java new file mode 100644 index 000000000..2b6ab373b --- /dev/null +++ b/src/main/java/com/zulrahhelper/options/MeleeStandLocation.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024, Ron Young + * 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 com.zulrahhelper.options; + +import java.awt.Color; +import java.awt.Graphics; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum MeleeStandLocation +{ + START(19, 52), + START_MAGMA(9, 47), + MID_LEFT(9, 32), + PILLAR_LEFT(19, 23), + TOP_LEFT(29, 5), + TOP_RIGHT(49, 5), + PILLAR_RIGHT(62, 23), + MID_RIGHT(69, 30); + + private static final int WIDTH = 6; + private static final int HEIGHT = 6; + + private final int x; + private final int y; + + public void drawX(Graphics g, int px, int py) + { + var x = px + this.x - WIDTH / 2; + var y = py + this.y - HEIGHT / 2; + + g.setColor(Color.WHITE); + + g.drawLine(x, y, x + WIDTH, y + HEIGHT); + g.drawLine(x, y + HEIGHT, x + WIDTH, y); + + g.drawLine(x - 1, y, x + WIDTH - 1, y + HEIGHT); + g.drawLine(x - 1, y + HEIGHT, x + WIDTH - 1, y); + } + + public int getX() + { + return x; + } + + public int getY() + { + return y; + } +} diff --git a/src/main/java/com/zulrahhelper/options/MeleeStep.java b/src/main/java/com/zulrahhelper/options/MeleeStep.java new file mode 100644 index 000000000..512f3728e --- /dev/null +++ b/src/main/java/com/zulrahhelper/options/MeleeStep.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024, Ron Young + * 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 com.zulrahhelper.options; + +import lombok.Value; + +@Value +public class MeleeStep +{ + MeleeStandLocation location; + ArrowDirection arrow; + + public MeleeStep(MeleeStandLocation location) + { + this(location, ArrowDirection.NONE); + } + + public MeleeStep(MeleeStandLocation location, ArrowDirection arrow) + { + this.location = location; + this.arrow = arrow; + } +} diff --git a/src/main/java/com/zulrahhelper/tree/PatternTree.java b/src/main/java/com/zulrahhelper/tree/PatternTree.java index 195826f64..c7ece6ba1 100644 --- a/src/main/java/com/zulrahhelper/tree/PatternTree.java +++ b/src/main/java/com/zulrahhelper/tree/PatternTree.java @@ -26,6 +26,9 @@ package com.zulrahhelper.tree; import com.google.common.collect.Iterables; +import com.zulrahhelper.options.ArrowDirection; +import com.zulrahhelper.options.MeleeStandLocation; +import com.zulrahhelper.options.MeleeStep; import com.zulrahhelper.options.Prayer; import com.zulrahhelper.options.StandLocation; import com.zulrahhelper.options.ZulrahForm; @@ -153,6 +156,7 @@ private static Node build() .form(ZulrahForm.RANGE) .attacks(5) .venom(4) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) .build()) .child(buildMelee()) .child(buildRange()) @@ -169,6 +173,8 @@ private static Node buildMelee() .point(StandLocation.START) .point(StandLocation.START_MAGMA) .attacks(2) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) + .meleeStep(new MeleeStep(MeleeStandLocation.START_MAGMA)) .build()) .child(Node.builder() .value(Step.builder() @@ -176,6 +182,8 @@ private static Node buildMelee() .prayer(Prayer.MAGIC) .attacks(4) .point(StandLocation.START) + .meleeStep(new MeleeStep(MeleeStandLocation.START, ArrowDirection.UP)) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_LEFT)) .build()) .child(buildMeleeA()) .child(buildMeleeB()) @@ -195,12 +203,16 @@ private static Node buildMeleeA() .attacks(5) .venom(2) .snakelings(2) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_LEFT, ArrowDirection.RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_RIGHT, ArrowDirection.DOWN_RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_RIGHT)) .build()) .node() .value(Step.builder() .form(ZulrahForm.MELEE) .attacks(2) .point(StandLocation.PILLAR_2_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_RIGHT)) .build()) .node() .value(Step.builder() @@ -209,6 +221,7 @@ private static Node buildMeleeA() .prayer(Prayer.MAGIC) .spawn(ZulrahLocation.EAST) .point(StandLocation.NORTH) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_RIGHT)) .build()) .node() .value(Step.builder() @@ -217,6 +230,7 @@ private static Node buildMeleeA() .venom(3) .snakelings(2) .point(StandLocation.PILLAR_1_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_LEFT)) .build()) .node() .value(Step.builder() @@ -227,6 +241,10 @@ private static Node buildMeleeA() .snakelings(2) .prayer(Prayer.MAGIC) .point(StandLocation.PILLAR_1_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_LEFT, ArrowDirection.UP_RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_LEFT, ArrowDirection.RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_RIGHT, ArrowDirection.DOWN_RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_RIGHT)) .build()) .node() .value(Step.builder() @@ -237,6 +255,7 @@ private static Node buildMeleeA() .prayer(Prayer.RANGE) .prayer(Prayer.MAGIC) .point(StandLocation.PILLAR_2_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_RIGHT)) .build()) .node() .value(Step.builder() @@ -244,6 +263,8 @@ private static Node buildMeleeA() .attacks(2) .point(StandLocation.START) .point(StandLocation.START_MAGMA) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) + .meleeStep(new MeleeStep(MeleeStandLocation.START_MAGMA)) .build()) .node() .value(Step.builder() @@ -255,6 +276,7 @@ private static Node buildMeleeA() .attacks(5) .venom(4) .reset(true) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) .build()) .buildUp(); } @@ -269,6 +291,7 @@ private static Node buildMeleeB() .point(StandLocation.PILLAR_2_SOUTH) .venom(3) .snakelings(2) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_RIGHT)) .build()) .node() .value(Step.builder() @@ -279,12 +302,15 @@ private static Node buildMeleeB() .snakelings(2) .prayer(Prayer.MAGIC) .point(StandLocation.PILLAR_2_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_RIGHT, ArrowDirection.DOWN_RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_RIGHT)) .build()) .node() .value(Step.builder() .form(ZulrahForm.MELEE) .attacks(2) .point(StandLocation.PILLAR_2_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_RIGHT)) .build()) .node() .value(Step.builder() @@ -293,6 +319,7 @@ private static Node buildMeleeB() .attacks(5) .prayer(Prayer.RANGE) .point(StandLocation.NORTH) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_LEFT)) .build()) .node() .value(Step.builder() @@ -303,6 +330,9 @@ private static Node buildMeleeB() .snakelings(2) .prayer(Prayer.MAGIC) .point(StandLocation.PILLAR_2_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_LEFT, ArrowDirection.RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_RIGHT, ArrowDirection.DOWN_RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_RIGHT)) .build()) .node() .value(Step.builder() @@ -313,6 +343,7 @@ private static Node buildMeleeB() .prayer(Prayer.RANGE) .prayer(Prayer.MAGIC) .point(StandLocation.PILLAR_2_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_RIGHT)) .build()) .node() .value(Step.builder() @@ -320,6 +351,8 @@ private static Node buildMeleeB() .attacks(2) .point(StandLocation.START) .point(StandLocation.START_MAGMA) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) + .meleeStep(new MeleeStep(MeleeStandLocation.START_MAGMA)) .build()) .node() .value(Step.builder() @@ -331,6 +364,7 @@ private static Node buildMeleeB() .attacks(5) .venom(4) .reset(true) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) .build()) .buildUp(); } @@ -346,6 +380,7 @@ private static Node buildRange() .prayer(Prayer.RANGE) .attacks(5) .snakelings(2) + .meleeStep(new MeleeStep(MeleeStandLocation.START_MAGMA)) .build()) .node() .value(Step.builder() @@ -354,6 +389,7 @@ private static Node buildRange() .venom(3) .snakelings(2) .point(StandLocation.PILLAR_1_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_LEFT)) .build()) .node() .value(Step.builder() @@ -362,6 +398,7 @@ private static Node buildRange() .attacks(5) .prayer(Prayer.MAGIC) .point(StandLocation.NORTH) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_RIGHT)) .build()) .node() .value(Step.builder() @@ -370,6 +407,8 @@ private static Node buildRange() .attacks(5) .prayer(Prayer.RANGE) .point(StandLocation.NORTH) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_RIGHT, ArrowDirection.LEFT)) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_LEFT)) .build()) .node() .value(Step.builder() @@ -378,6 +417,7 @@ private static Node buildRange() .attacks(5) .prayer(Prayer.MAGIC) .point(StandLocation.NORTH) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_LEFT)) .build()) .node() .value(Step.builder() @@ -385,6 +425,7 @@ private static Node buildRange() .venom(3) .snakelings(2) .point(StandLocation.PILLAR_2_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_RIGHT)) .build()) .node() .value(Step.builder() @@ -393,6 +434,7 @@ private static Node buildRange() .attacks(5) .prayer(Prayer.RANGE) .point(StandLocation.PILLAR_2_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_RIGHT)) .build()) .node() .value(Step.builder() @@ -402,6 +444,7 @@ private static Node buildRange() .snakelings(2) .prayer(Prayer.MAGIC) .point(StandLocation.PILLAR_1_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_LEFT)) .build()) .node() .value(Step.builder() @@ -411,12 +454,14 @@ private static Node buildRange() .prayer(Prayer.MAGIC) .prayer(Prayer.RANGE) .point(StandLocation.PILLAR_1_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_LEFT)) .build()) .node() .value(Step.builder() .form(ZulrahForm.MAGE) .snakelings(2) .point(StandLocation.START) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) .build()) .node() .value(Step.builder() @@ -428,6 +473,7 @@ private static Node buildRange() .attacks(5) .venom(4) .reset(true) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) .build()) .buildUp(); } @@ -443,6 +489,7 @@ private static Node buildMage() .prayer(Prayer.MAGIC) .attacks(6) .snakelings(2) + .meleeStep(new MeleeStep(MeleeStandLocation.START_MAGMA)) .build()) .node() .value(Step.builder() @@ -452,6 +499,9 @@ private static Node buildMage() .venom(2) .prayer(Prayer.RANGE) .point(StandLocation.PILLAR_2_SOUTH) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_LEFT, ArrowDirection.LEFT)) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_RIGHT, ArrowDirection.DOWN_RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_RIGHT)) .build()) .node() .value(Step.builder() @@ -461,6 +511,7 @@ private static Node buildMage() .point(StandLocation.PILLAR_2_SOUTH) .attacks(4) .snakelings(2) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_RIGHT)) .build()) .node() .value(Step.builder() @@ -468,6 +519,7 @@ private static Node buildMage() .point(StandLocation.PILLAR_1_SOUTH) .attacks(2) .venom(2) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_LEFT)) .build()) .node() .value(Step.builder() @@ -476,6 +528,7 @@ private static Node buildMage() .point(StandLocation.PILLAR_1_SOUTH) .prayer(Prayer.RANGE) .attacks(4) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_LEFT)) .build()) .node() .value(Step.builder() @@ -484,6 +537,10 @@ private static Node buildMage() .point(StandLocation.PILLAR_1_SOUTH) .snakelings(2) .venom(3) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_LEFT, ArrowDirection.UP_RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_LEFT, ArrowDirection.RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.TOP_RIGHT, ArrowDirection.DOWN_RIGHT)) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_RIGHT)) .build()) .node() .value(Step.builder() @@ -493,6 +550,7 @@ private static Node buildMage() .point(StandLocation.PILLAR_2_EAST) .attacks(5) .venom(4) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_RIGHT)) .build()) .node() .value(Step.builder() @@ -500,6 +558,7 @@ private static Node buildMage() .prayer(Prayer.RANGE) .point(StandLocation.PILLAR_1_SOUTH) .attacks(5) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_LEFT)) .build()) .node() .value(Step.builder() @@ -508,6 +567,7 @@ private static Node buildMage() .point(StandLocation.PILLAR_1_SOUTH) .attacks(4) .venom(3) + .meleeStep(new MeleeStep(MeleeStandLocation.PILLAR_LEFT)) .build()) .node() .value(Step.builder() @@ -517,12 +577,14 @@ private static Node buildMage() .prayer(Prayer.RANGE) .point(StandLocation.PILLAR_1_SOUTH) .attacks(8) + .meleeStep(new MeleeStep(MeleeStandLocation.MID_LEFT)) .build()) .node() .value(Step.builder() .form(ZulrahForm.MAGE) .point(StandLocation.START) .snakelings(2) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) .build()) .node() .value(Step.builder() @@ -534,6 +596,7 @@ private static Node buildMage() .attacks(5) .venom(4) .reset(true) + .meleeStep(new MeleeStep(MeleeStandLocation.START)) .build()) .buildUp(); } diff --git a/src/main/java/com/zulrahhelper/tree/Step.java b/src/main/java/com/zulrahhelper/tree/Step.java index b5e30202e..85ac11917 100644 --- a/src/main/java/com/zulrahhelper/tree/Step.java +++ b/src/main/java/com/zulrahhelper/tree/Step.java @@ -25,6 +25,7 @@ package com.zulrahhelper.tree; +import com.zulrahhelper.options.MeleeStep; import com.zulrahhelper.options.Prayer; import com.zulrahhelper.options.StandLocation; import com.zulrahhelper.options.ZulrahForm; @@ -63,4 +64,7 @@ public class Step ZulrahLocation spawn = ZulrahLocation.SOUTH; boolean reset; + + @Singular + List meleeSteps; } diff --git a/src/main/java/com/zulrahhelper/ui/Images.java b/src/main/java/com/zulrahhelper/ui/Images.java index 23b384b8f..96c885519 100644 --- a/src/main/java/com/zulrahhelper/ui/Images.java +++ b/src/main/java/com/zulrahhelper/ui/Images.java @@ -25,23 +25,25 @@ package com.zulrahhelper.ui; -import com.zulrahhelper.ZulrahHelperConfig; -import com.zulrahhelper.ZulrahHelperPlugin; -import com.zulrahhelper.tree.Step; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; + +import com.zulrahhelper.ZulrahHelperConfig; +import com.zulrahhelper.ZulrahHelperPlugin; +import com.zulrahhelper.options.ArrowDirection; +import com.zulrahhelper.tree.Step; + import net.runelite.client.ui.FontManager; import net.runelite.client.util.ImageUtil; -public class Images -{ +public class Images { private static final BufferedImage FLOOR_IMG = ImageUtil.loadImageResource(Step.class, "/floor.png"); private static final BufferedImage SNAKELINGS = ImageUtil.loadImageResource(Step.class, "/options/snakeling2.png"); private static final BufferedImage HITSPLAT = ImageUtil.loadImageResource(Step.class, "/options/hitsplat.png"); private static final BufferedImage VENOM = ImageUtil.loadImageResource(Step.class, "/options/venom.png"); - private static final BufferedImage RESET = ImageUtil.loadImageResource(ZulrahHelperPlugin.class, "/ui/reset_icon.png");; - + private static final BufferedImage RESET = ImageUtil.loadImageResource(ZulrahHelperPlugin.class, "/ui/reset_icon.png"); + ; private static final int WIDTH = 105; private static final int HEIGHT = 105; @@ -51,8 +53,7 @@ public class Images private static final Color DARK_BACKGROUND = new Color(24, 24, 24, 65); private static final Color LIGHT_BACKGROUND = new Color(255, 255, 255, 200); - public static BufferedImage createImage(final Step step, final ZulrahHelperConfig config) - { + public static BufferedImage createImage(final Step step, final ZulrahHelperConfig config) { BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) image.getGraphics(); @@ -64,27 +65,44 @@ public static BufferedImage createImage(final Step step, final ZulrahHelperConfi g.drawImage(FLOOR_IMG, null, px, py); - for (var p : step.getPoints()) - { - p.drawX(g, px, py); + if(config.usingMelee() && !step.getMeleeSteps().isEmpty()) { + // Draw melee positions with arrows + var meleeSteps = step.getMeleeSteps(); + for(int i = 0; i < meleeSteps.size(); i++) { + var meleeStep = meleeSteps.get(i); + meleeStep.getLocation().drawX(g, px, py); + + // Draw arrow to next position if not last + if(i < meleeSteps.size() - 1 && meleeStep.getArrow() != ArrowDirection.NONE) { + var currentLoc = meleeStep.getLocation(); + var nextLoc = meleeSteps.get(i + 1).getLocation(); + meleeStep.getArrow().drawArrow(g, + px + currentLoc.getX(), py + currentLoc.getY(), + px + nextLoc.getX(), py + nextLoc.getY()); + } + } + } else { + // Original ranged mode (or melee fallback if no melee steps defined) + for(var p : step.getPoints()) { + p.drawX(g, px, py); + } } var spawn = step.getSpawn(); - spawn.drawLocation(g, step.getForm().getColor(config), px, py); + if(spawn != null && step.getForm() != null) { + spawn.drawLocation(g, step.getForm().getColor(config), px, py); + } var theta = config.imageOrientation().getRotation(); - if (theta != 0) - { + if(theta != 0) { image = ImageUtil.rotateImage(image, theta); g.dispose(); g = (Graphics2D) image.getGraphics(); } var prayers = step.getPrayers(); - if (config.displayPrayerIcons()) - { - for (int idx = 0; idx < prayers.size(); idx++) - { + if(config.displayPrayerIcons()) { + for(int idx = 0; idx < prayers.size(); idx++) { var p = prayers.get(idx); var img = p.getImage(); @@ -94,25 +112,21 @@ public static BufferedImage createImage(final Step step, final ZulrahHelperConfi } g.setFont(FontManager.getRunescapeBoldFont()); - if (config.displayAttackIcons() && step.getAttacks() > 0) - { + if(config.displayAttackIcons() && step.getAttacks() > 0) { drawSplat(g, Color.WHITE, HITSPLAT, step.getAttacks() + "", 0); } - if (config.displayVenom() && step.getVenom() > 0) - { + if(config.displayVenom() && step.getVenom() > 0) { drawSplat(g, Color.WHITE, VENOM, step.getVenom() + "", WIDTH / 2 - VENOM.getWidth() / 2); } - if (config.displaySnakelings() && step.getSnakelings() > 0) - { + if(config.displaySnakelings() && step.getSnakelings() > 0) { var c = config.darkMode() ? Color.WHITE : Color.BLACK; var x = WIDTH - SNAKELINGS.getWidth() * 2 + PADDING * 2; drawSplat(g, c, SNAKELINGS, step.getSnakelings() + "", x, x + PADDING); } - if (step.isReset()) - { + if(step.isReset()) { var p = 23; g.drawImage(RESET, p, p, RESET.getWidth() * 3, RESET.getHeight() * 3, null); } @@ -121,8 +135,7 @@ public static BufferedImage createImage(final Step step, final ZulrahHelperConfi return image; } - private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String text, int splatX, int textX) - { + private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String text, int splatX, int textX) { var fm = g.getFontMetrics(); g.drawImage(img, null, splatX + PADDING, HEIGHT - PADDING - HITSPLAT.getHeight()); var tw = fm.stringWidth(text); @@ -139,8 +152,7 @@ private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String t g.drawString(text, tx, ty); } - private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String text, int x) - { + private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String text, int x) { drawSplat(g, c, img, text, x, x); } } diff --git a/src/test/java/com/zulrahhelper/ImageBuilderTest.java b/src/test/java/com/zulrahhelper/ImageBuilderTest.java index c3dada9d0..16e1ef787 100644 --- a/src/test/java/com/zulrahhelper/ImageBuilderTest.java +++ b/src/test/java/com/zulrahhelper/ImageBuilderTest.java @@ -25,11 +25,14 @@ package com.zulrahhelper; +import com.zulrahhelper.options.MeleeStandLocation; +import com.zulrahhelper.options.MeleeStep; import com.zulrahhelper.options.Prayer; import com.zulrahhelper.options.StandLocation; import com.zulrahhelper.options.ZulrahForm; import com.zulrahhelper.options.ZulrahLocation; import com.zulrahhelper.tree.Node; +import com.zulrahhelper.tree.PatternTree; import com.zulrahhelper.tree.Step; import com.zulrahhelper.ui.Images; import java.awt.image.BufferedImage; @@ -40,6 +43,7 @@ import static org.junit.Assert.assertEquals; import org.junit.Test; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; @Slf4j public class ImageBuilderTest @@ -49,6 +53,13 @@ private ZulrahHelperConfig getConfig() return spy(ZulrahHelperConfig.class); } + private ZulrahHelperConfig getMeleeConfig() + { + var config = spy(ZulrahHelperConfig.class); + when(config.usingMelee()).thenReturn(true); + return config; + } + @Test public void streamlinedBuilder() { @@ -165,4 +176,70 @@ public void points() throws IOException ImageIO.write(img, "png", out); } } + + @Test + public void meleePoints() throws IOException + { + new File("out/melee").mkdirs(); + var config = getMeleeConfig(); + + for (var loc : MeleeStandLocation.values()) + { + Step step = Step.builder() + .form(ZulrahForm.RANGE) + .meleeStep(new MeleeStep(loc)) + .build(); + + BufferedImage img = Images.createImage(step, config); + + File out = new File("out/melee/" + loc + ".png"); + ImageIO.write(img, "png", out); + } + } + + @Test + public void meleeRotations() throws IOException + { + new File("out/ranged/rotations").mkdirs(); + new File("out/melee/rotations").mkdirs(); + walkTree(new PatternTree().getState(), "START", 0); + } + + private void walkTree(Node node, String section, int stepNum) throws IOException + { + if (node == null) + { + return; + } + + Step step = node.getValue(); + String title = step.getTitle(); + + String currentSection = section; + int currentStep = stepNum + 1; + String filename; + + if (title != null && !title.equals("RESET")) + { + currentSection = title.replace(" ", "_"); + currentStep = 1; + } + + if ("RESET".equals(title)) + { + filename = section + "_RESET"; + } + else + { + filename = currentSection + "_" + String.format("%02d", currentStep); + } + + ImageIO.write(Images.createImage(step, getConfig()), "png", new File("out/ranged/rotations/" + filename + ".png")); + ImageIO.write(Images.createImage(step, getMeleeConfig()), "png", new File("out/melee/rotations/" + filename + ".png")); + + for (var child : node.getChildren()) + { + walkTree(child, currentSection, currentStep); + } + } } From 70ff20a4ea760782419c388b0381fbe2908390f7 Mon Sep 17 00:00:00 2001 From: KowabungaCheese Date: Fri, 12 Jun 2026 21:28:04 -0500 Subject: [PATCH 2/2] Restored proper formatting --- src/main/java/com/zulrahhelper/ui/Images.java | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/zulrahhelper/ui/Images.java b/src/main/java/com/zulrahhelper/ui/Images.java index 96c885519..2bab1fe3b 100644 --- a/src/main/java/com/zulrahhelper/ui/Images.java +++ b/src/main/java/com/zulrahhelper/ui/Images.java @@ -25,25 +25,23 @@ package com.zulrahhelper.ui; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; - import com.zulrahhelper.ZulrahHelperConfig; import com.zulrahhelper.ZulrahHelperPlugin; import com.zulrahhelper.options.ArrowDirection; import com.zulrahhelper.tree.Step; - +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; import net.runelite.client.ui.FontManager; import net.runelite.client.util.ImageUtil; -public class Images { +public class Images +{ private static final BufferedImage FLOOR_IMG = ImageUtil.loadImageResource(Step.class, "/floor.png"); private static final BufferedImage SNAKELINGS = ImageUtil.loadImageResource(Step.class, "/options/snakeling2.png"); private static final BufferedImage HITSPLAT = ImageUtil.loadImageResource(Step.class, "/options/hitsplat.png"); private static final BufferedImage VENOM = ImageUtil.loadImageResource(Step.class, "/options/venom.png"); private static final BufferedImage RESET = ImageUtil.loadImageResource(ZulrahHelperPlugin.class, "/ui/reset_icon.png"); - ; private static final int WIDTH = 105; private static final int HEIGHT = 105; @@ -53,7 +51,8 @@ public class Images { private static final Color DARK_BACKGROUND = new Color(24, 24, 24, 65); private static final Color LIGHT_BACKGROUND = new Color(255, 255, 255, 200); - public static BufferedImage createImage(final Step step, final ZulrahHelperConfig config) { + public static BufferedImage createImage(final Step step, final ZulrahHelperConfig config) + { BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) image.getGraphics(); @@ -65,44 +64,51 @@ public static BufferedImage createImage(final Step step, final ZulrahHelperConfi g.drawImage(FLOOR_IMG, null, px, py); - if(config.usingMelee() && !step.getMeleeSteps().isEmpty()) { - // Draw melee positions with arrows + if (config.usingMelee() && !step.getMeleeSteps().isEmpty()) + { var meleeSteps = step.getMeleeSteps(); - for(int i = 0; i < meleeSteps.size(); i++) { + for (int i = 0; i < meleeSteps.size(); i++) + { var meleeStep = meleeSteps.get(i); meleeStep.getLocation().drawX(g, px, py); - // Draw arrow to next position if not last - if(i < meleeSteps.size() - 1 && meleeStep.getArrow() != ArrowDirection.NONE) { + if (i < meleeSteps.size() - 1 && meleeStep.getArrow() != ArrowDirection.NONE) + { var currentLoc = meleeStep.getLocation(); var nextLoc = meleeSteps.get(i + 1).getLocation(); meleeStep.getArrow().drawArrow(g, - px + currentLoc.getX(), py + currentLoc.getY(), - px + nextLoc.getX(), py + nextLoc.getY()); + px + currentLoc.getX(), py + currentLoc.getY(), + px + nextLoc.getX(), py + nextLoc.getY()); } } - } else { - // Original ranged mode (or melee fallback if no melee steps defined) - for(var p : step.getPoints()) { + } + else + { + for (var p : step.getPoints()) + { p.drawX(g, px, py); } } var spawn = step.getSpawn(); - if(spawn != null && step.getForm() != null) { + if (spawn != null && step.getForm() != null) + { spawn.drawLocation(g, step.getForm().getColor(config), px, py); } var theta = config.imageOrientation().getRotation(); - if(theta != 0) { + if (theta != 0) + { image = ImageUtil.rotateImage(image, theta); g.dispose(); g = (Graphics2D) image.getGraphics(); } var prayers = step.getPrayers(); - if(config.displayPrayerIcons()) { - for(int idx = 0; idx < prayers.size(); idx++) { + if (config.displayPrayerIcons()) + { + for (int idx = 0; idx < prayers.size(); idx++) + { var p = prayers.get(idx); var img = p.getImage(); @@ -112,21 +118,25 @@ public static BufferedImage createImage(final Step step, final ZulrahHelperConfi } g.setFont(FontManager.getRunescapeBoldFont()); - if(config.displayAttackIcons() && step.getAttacks() > 0) { + if (config.displayAttackIcons() && step.getAttacks() > 0) + { drawSplat(g, Color.WHITE, HITSPLAT, step.getAttacks() + "", 0); } - if(config.displayVenom() && step.getVenom() > 0) { + if (config.displayVenom() && step.getVenom() > 0) + { drawSplat(g, Color.WHITE, VENOM, step.getVenom() + "", WIDTH / 2 - VENOM.getWidth() / 2); } - if(config.displaySnakelings() && step.getSnakelings() > 0) { + if (config.displaySnakelings() && step.getSnakelings() > 0) + { var c = config.darkMode() ? Color.WHITE : Color.BLACK; var x = WIDTH - SNAKELINGS.getWidth() * 2 + PADDING * 2; drawSplat(g, c, SNAKELINGS, step.getSnakelings() + "", x, x + PADDING); } - if(step.isReset()) { + if (step.isReset()) + { var p = 23; g.drawImage(RESET, p, p, RESET.getWidth() * 3, RESET.getHeight() * 3, null); } @@ -135,7 +145,8 @@ public static BufferedImage createImage(final Step step, final ZulrahHelperConfi return image; } - private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String text, int splatX, int textX) { + private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String text, int splatX, int textX) + { var fm = g.getFontMetrics(); g.drawImage(img, null, splatX + PADDING, HEIGHT - PADDING - HITSPLAT.getHeight()); var tw = fm.stringWidth(text); @@ -152,7 +163,8 @@ private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String t g.drawString(text, tx, ty); } - private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String text, int x) { + private static void drawSplat(Graphics2D g, Color c, BufferedImage img, String text, int x) + { drawSplat(g, c, img, text, x, x); } }