diff --git a/README.md b/README.md index 214b58c..26eee5e 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,18 @@ Font style and size can be controlled by the matching options. </details> +<details> +<summary>Kephri</summary> + +### 🚀 Swarmer + +Displays wave numbers on scarab swarms in the kephri room as they spawn. +The side panel will show logs of previous raid's leaked swarms. +Font color, style and size can be controlled by the matching options. +![Swarmer Example](https://imgur.com/msneEOQ.png "Swarmer Example") + +</details> + <details> <summary>Path of Apmeken</summary> diff --git a/src/main/java/com/duckblade/osrs/toa/TombsOfAmascutConfig.java b/src/main/java/com/duckblade/osrs/toa/TombsOfAmascutConfig.java index d2d9332..eaeb413 100644 --- a/src/main/java/com/duckblade/osrs/toa/TombsOfAmascutConfig.java +++ b/src/main/java/com/duckblade/osrs/toa/TombsOfAmascutConfig.java @@ -1,6 +1,8 @@ package com.duckblade.osrs.toa; import com.duckblade.osrs.toa.features.QuickProceedSwaps.QuickProceedEnableMode; +import com.duckblade.osrs.toa.features.boss.kephri.swarmer.SwarmerFonts; +import com.duckblade.osrs.toa.features.boss.kephri.swarmer.SwarmerPanelManager; import com.duckblade.osrs.toa.features.hporbs.HpOrbMode; import com.duckblade.osrs.toa.features.scabaras.ScabarasHelperMode; import com.duckblade.osrs.toa.features.scabaras.SkipObeliskOverlay; @@ -9,7 +11,9 @@ import com.duckblade.osrs.toa.features.updatenotifier.UpdateNotifier; import com.duckblade.osrs.toa.util.FontStyle; import com.duckblade.osrs.toa.util.HighlightMode; -import java.awt.Color; + +import java.awt.*; + import net.runelite.client.config.Alpha; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; @@ -42,10 +46,18 @@ public interface TombsOfAmascutConfig extends Config ) String SECTION_AKKHA = "sectionAkkha"; + @ConfigSection( + name = "Kephri", + description = "Configuration for the Kephri room.", + position = 2, + closedByDefault = true + ) + String SECTION_KEPHRI = "sectionKephri"; + @ConfigSection( name = "Path of Apmeken", description = "Options for the Path of Apmeken.", - position = 2, + position = 3, closedByDefault = true ) String SECTION_APMEKEN = "sectionApmeken"; @@ -53,7 +65,7 @@ public interface TombsOfAmascutConfig extends Config @ConfigSection( name = "Path of Het", description = "Helpers for the Path of Het.", - position = 3, + position = 4, closedByDefault = true ) String SECTION_HET = "sectionHet"; @@ -61,7 +73,7 @@ public interface TombsOfAmascutConfig extends Config @ConfigSection( name = "Path of Scabaras", description = "Options for the puzzles in the Path of Scabaras.", - position = 4, + position = 5, closedByDefault = true ) String SECTION_SCABARAS = "sectionScabaras"; @@ -69,7 +81,7 @@ public interface TombsOfAmascutConfig extends Config @ConfigSection( name = "Burial Tomb", description = "Configuration for the burial tomb.", - position = 5, + position = 6, closedByDefault = true ) String SECTION_BURIAL_TOMB = "sectionBurialTomb"; @@ -78,7 +90,7 @@ public interface TombsOfAmascutConfig extends Config name = "Points Tracker", description = "<html>Tracks points for the raid, used in calculating drop chance." + "<br/>NOTE: For teams, you MUST use the RuneLite Party plugin to receive team drop chance.</html>", - position = 6, + position = 7, closedByDefault = true ) String SECTION_POINTS_TRACKER = "sectionPointsTracker"; @@ -86,7 +98,7 @@ public interface TombsOfAmascutConfig extends Config @ConfigSection( name = "Invocation Presets", description = "Save presets of invocations to quickly restore your invocations between runs of different types.", - position = 7, + position = 8, closedByDefault = true ) String SECTION_INVOCATION_PRESETS = "invocationPresetsSection"; @@ -94,7 +106,7 @@ public interface TombsOfAmascutConfig extends Config @ConfigSection( name = "Invocation Screenshot", description = "All config options related to the Invocation Screenshot functionality", - position = 8, + position = 9, closedByDefault = true ) String SECTION_INVOCATION_SCREENSHOT = "invocationScreenshotSection"; @@ -102,7 +114,7 @@ public interface TombsOfAmascutConfig extends Config @ConfigSection( name = "Time Tracking", description = "Time tracking and splits.", - position = 9, + position = 10, closedByDefault = true ) String SECTION_TIME_TRACKING = "sectionTimeTracking"; @@ -147,6 +159,81 @@ default int akkhaFontSize() return 12; } + // Kephri + @ConfigItem( + keyName = "swarmerOverlay", + name = "Swarmer Overlay", + description = "Overlay swarm wave number.", + position = 0, + section = SECTION_KEPHRI + ) + default boolean swarmerOverlay() + { + return false; + } + + @ConfigItem( + name = "Font Type", + description = "Type of font", + position = 1, + keyName = "fontType", + section = SECTION_KEPHRI + ) + default SwarmerFonts swarmerFontType() + { + return SwarmerFonts.ARIAL; + } + + @ConfigItem( + name = "Use Bold Font", + description = "Font style of swarm overlay.", + position = 2, + keyName = "useBoldFont", + section = SECTION_KEPHRI + ) + default boolean useBoldFont() + { + return true; + } + + @ConfigItem( + name = "Font Size", + description = "Font size of swarm overlay.", + position = 3, + keyName = "swarmerFontSize", + section = SECTION_KEPHRI + ) + @Units(Units.PIXELS) + @Range(min = 12) + default int swarmerFontSize() + { + return 12; + } + + @ConfigItem( + name = "Side Panel", + description = "Show a side panel with summary data for previous raids.", + position = 4, + keyName = "swarmerSidePanel", + section = SECTION_KEPHRI + ) + default SwarmerPanelManager.PanelMode swarmerSidePanel() + { + return SwarmerPanelManager.PanelMode.NEVER; + } + + @ConfigItem( + name = "Font Color", + description = "Font color of swarm overlay.", + position = 4, + keyName = "swarmerFontColor", + section = SECTION_KEPHRI + ) + default Color swarmerFontColor() + { + return Color.WHITE; + } + // Apmeken @ConfigItem( diff --git a/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmNpc.java b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmNpc.java new file mode 100644 index 0000000..45088dd --- /dev/null +++ b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmNpc.java @@ -0,0 +1,11 @@ +package com.duckblade.osrs.toa.features.boss.kephri.swarmer; + +import lombok.Data; +import net.runelite.api.NPC; + +@Data +public class SwarmNpc +{ + private final NPC npc; + private final int waveSpawned; +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerDataManager.java b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerDataManager.java new file mode 100644 index 0000000..036f142 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerDataManager.java @@ -0,0 +1,133 @@ +package com.duckblade.osrs.toa.features.boss.kephri.swarmer; + +import com.duckblade.osrs.toa.TombsOfAmascutPlugin; +import com.duckblade.osrs.toa.module.PluginLifecycleComponent; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.io.FileReader; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.RequiredArgsConstructor; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class SwarmerDataManager implements PluginLifecycleComponent +{ + + private static final int MAX_RECENT_RAIDS = 10; + public static final Path SWARMS_DIRECTORY = new File(TombsOfAmascutPlugin.TOA_FOLDER, "kephri-swarms").toPath(); + + private final Gson gson; + + private ExecutorService executor; + + @Override + public void startUp() + { + executor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("ToA-SwarmerDataManager-%d").build()); + } + + @Override + public void shutDown() + { + executor.shutdown(); + } + + public CompletableFuture<List<String>> getRaidList() + { + return CompletableFuture.supplyAsync(() -> + { + try + { + if (!Files.exists(SWARMS_DIRECTORY)) + { + return Collections.emptyList(); + } + + try (Stream<Path> files = Files.list(SWARMS_DIRECTORY)) + { + return files.filter(f -> f.getFileName().toString().endsWith(".json")) + .sorted(Comparator.reverseOrder()) + .limit(MAX_RECENT_RAIDS) + .map(f -> f.getFileName().toString().replace(".json", "")) + .map(s -> s.replace('_', ':')) + .collect(Collectors.toList()); + } + } + catch (Exception ignored) + { + } + + return Collections.emptyList(); + }, executor); + } + + public CompletableFuture<List<SwarmerRoomData>> getRaidData(String raidUnsafe) + { + return CompletableFuture.supplyAsync(() -> + { + String raid = raidUnsafe.replace(':', '_'); + + if (!Files.exists(SWARMS_DIRECTORY)) + { + return Collections.emptyList(); + } + if (!Files.exists(SWARMS_DIRECTORY.resolve(raid + ".json"))) + { + return Collections.emptyList(); + } + + try (FileReader reader = new FileReader(SWARMS_DIRECTORY.resolve(raid + ".json").toFile())) + { + Type listType = new TypeToken<List<SwarmerRoomData>>() + { + }.getType(); + return gson.fromJson(reader, listType); + } + catch (Exception ignored) + { + return Collections.emptyList(); + } + }, executor); + } + + public CompletableFuture<Void> saveRaidData(List<SwarmerRoomData> raidDataList) + { + return CompletableFuture.runAsync(() -> + { + String raidName = new SimpleDateFormat("yyyy-MM-dd HH_mm_ss").format(new Date()); + try + { + if (!Files.exists(SWARMS_DIRECTORY)) + { + Files.createDirectories(SWARMS_DIRECTORY); + } + Files.writeString( + SWARMS_DIRECTORY.resolve(raidName + ".json"), + gson.toJson(raidDataList), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + ); + } + catch (Exception ignored) + { + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerFonts.java b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerFonts.java new file mode 100644 index 0000000..1127f0a --- /dev/null +++ b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerFonts.java @@ -0,0 +1,30 @@ +package com.duckblade.osrs.toa.features.boss.kephri.swarmer; + +import lombok.Getter; + +@Getter +public enum SwarmerFonts +{ + REGULAR("RS Regular"), + ARIAL("Arial"), + CAMBRIA("Cambria"), + ROCKWELL("Rockwell"), + SEGOE_UI("Segoe Ui"), + TIMES_NEW_ROMAN("Times New Roman"), + VERDANA("Verdana"), + DIALOG("DIALOG"), + RUNESCAPE("RuneScape"); + + private final String name; + + public String toString() + { + return this.name; + } + + SwarmerFonts(String name) + { + this.name = name; + } + +} diff --git a/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerOverlay.java b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerOverlay.java new file mode 100644 index 0000000..be274ab --- /dev/null +++ b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerOverlay.java @@ -0,0 +1,274 @@ +package com.duckblade.osrs.toa.features.boss.kephri.swarmer; + +import com.duckblade.osrs.toa.TombsOfAmascutConfig; +import com.duckblade.osrs.toa.module.PluginLifecycleComponent; +import com.duckblade.osrs.toa.util.RaidRoom; +import com.duckblade.osrs.toa.util.RaidState; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.font.FontRenderContext; +import java.awt.font.TextLayout; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.inject.Inject; +import javax.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.NPC; +import net.runelite.api.NpcID; +import net.runelite.api.Point; +import net.runelite.api.events.AnimationChanged; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.NpcSpawned; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.ui.overlay.Overlay; +import net.runelite.client.ui.overlay.OverlayLayer; +import net.runelite.client.ui.overlay.OverlayManager; +import net.runelite.client.ui.overlay.OverlayPosition; + +@Slf4j +@Singleton +public class SwarmerOverlay extends Overlay implements PluginLifecycleComponent +{ + private static final int ANIMATION_KEPHRI_DOWN = 9579; + private static final int ANIMATION_KEPHRI_UP = 9581; + private static final int ANIMATION_SWARM_LEAK = 9607; + private static final int ANIMATION_SWARM_DEATH = 9608; + + private static final String ROOM_ENDED_MESSAGE = "Challenge complete: Kephri."; + private static final String ROOM_FAIL_MESSAGE = "Your party failed to complete"; + + private final Client client; + private final EventBus eventBus; + private final OverlayManager overlayManager; + private final TombsOfAmascutConfig config; + private final SwarmerDataManager swarmerDataManager; + private final SwarmerPanel swarmerPanel; + + private final Map<Integer, SwarmNpc> aliveSwarms = new HashMap<>(); + private final Map<Integer, Map<Integer, Integer>> leaks = new HashMap<>(); + + private int waveNumber; + private int kephriDownCount; + + private boolean isKephriDowned; + private int lastSpawnTick; + + @Inject + public SwarmerOverlay(Client client, EventBus eventBus, OverlayManager overlayManager, TombsOfAmascutConfig config, SwarmerDataManager swarmerDataManager, SwarmerPanel swarmerPanel) + { + this.client = client; + this.eventBus = eventBus; + this.overlayManager = overlayManager; + this.config = config; + this.swarmerDataManager = swarmerDataManager; + this.swarmerPanel = swarmerPanel; + + setPriority(Overlay.PRIORITY_HIGH); + setPosition(OverlayPosition.DYNAMIC); + setLayer(OverlayLayer.ABOVE_SCENE); + } + + @Override + public boolean isEnabled(TombsOfAmascutConfig config, RaidState raidState) + { + return config.swarmerOverlay() + && raidState.getCurrentRoom() == RaidRoom.KEPHRI; + } + + @Override + public void startUp() + { + eventBus.register(this); + overlayManager.add(this); + reset(); + } + + @Override + public void shutDown() + { + eventBus.unregister(this); + overlayManager.remove(this); + reset(); + } + + private void reset() + { + leaks.clear(); + aliveSwarms.clear(); + isKephriDowned = false; + lastSpawnTick = -1; + waveNumber = 0; + kephriDownCount = 0; + } + + @Subscribe + public void onNpcSpawned(NpcSpawned event) + { + final NPC npc = event.getNpc(); + final int npcId = npc.getId(); + + if (isKephriDowned && npcId == NpcID.SCARAB_SWARM_11723) + { + int thisTick = client.getTickCount(); + if (lastSpawnTick != thisTick) + { + waveNumber++; + lastSpawnTick = thisTick; + } + + SwarmNpc swarm = new SwarmNpc(npc, waveNumber); + aliveSwarms.put(npc.getIndex(), swarm); + } + } + + @Subscribe + public void onAnimationChanged(AnimationChanged e) + { + if (!(e.getActor() instanceof NPC)) + { + return; + } + + NPC npc = ((NPC) e.getActor()); + if (npc.getId() == NpcID.SCARAB_SWARM_11723) + { + handleSwarmAnimationChanged(npc); + } + else if (npc.getId() == NpcID.KEPHRI || npc.getId() == NpcID.KEPHRI_11720) + { + handleKephriAnimationChanged(npc); + } + } + + private void handleSwarmAnimationChanged(NPC npc) + { + SwarmNpc swarm = aliveSwarms.get(npc.getIndex()); + if (swarm == null) + { + return; + } + + if (npc.getAnimation() == ANIMATION_SWARM_LEAK) + { + aliveSwarms.remove(npc.getIndex()); + leaks.compute(kephriDownCount, (downs, waveMap) -> + { + Map<Integer, Integer> waveLeaks = waveMap != null ? waveMap : new HashMap<>(); + waveLeaks.compute(swarm.getWaveSpawned(), (wave, count) -> count != null ? count + 1 : 1); + return waveLeaks; + }); + } + else if (npc.getAnimation() == ANIMATION_SWARM_DEATH) + { + aliveSwarms.remove(npc.getIndex()); + } + } + + private void handleKephriAnimationChanged(NPC npc) + { + if (!isKephriDowned && npc.getAnimation() == ANIMATION_KEPHRI_DOWN) + { + isKephriDowned = true; + kephriDownCount++; + waveNumber = 0; + aliveSwarms.clear(); + } + else if (isKephriDowned && npc.getAnimation() == ANIMATION_KEPHRI_UP) + { + isKephriDowned = false; + } + } + + @Subscribe + public void onChatMessage(ChatMessage event) + { + if (!event.getType().equals(ChatMessageType.GAMEMESSAGE)) + { + return; + } + + if (event.getMessage().startsWith(ROOM_ENDED_MESSAGE)) + { + List<SwarmerRoomData> swarmData = new ArrayList<>(); + for (Map.Entry<Integer, Map<Integer, Integer>> e : leaks.entrySet()) + { + int down = e.getKey(); + for (Map.Entry<Integer, Integer> f : e.getValue().entrySet()) + { + int wave = f.getKey(); + int leaks = f.getValue(); + swarmData.add(new SwarmerRoomData(down, wave, leaks)); + } + } + swarmerDataManager.saveRaidData(swarmData) + .thenRun(() -> + { + if (config.swarmerSidePanel() != SwarmerPanelManager.PanelMode.NEVER) + { + swarmerPanel.updateRecentRaids(); + } + }); + } + + if (event.getMessage().startsWith(ROOM_FAIL_MESSAGE)) + { + reset(); + } + } + + @Override + public Dimension render(Graphics2D graphics) + { + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + aliveSwarms.values() + .stream() + .collect(Collectors.groupingBy(swarm -> swarm.getNpc().getWorldLocation())) + .values() + .forEach(tileSwarms -> + { + int stackOffset = 0; + for (SwarmNpc swarm : tileSwarms) + { + this.draw(graphics, swarm, stackOffset); + stackOffset += graphics.getFontMetrics().getHeight(); + } + }); + return null; + } + + private void draw(Graphics2D graphics, SwarmNpc swarmer, int offset) + { + String text = String.valueOf(swarmer.getWaveSpawned()); + + Point canvasTextLocation = swarmer.getNpc().getCanvasTextLocation(graphics, text, 0); + if (canvasTextLocation == null) + { + return; + } + int x = canvasTextLocation.getX(); + int y = canvasTextLocation.getY() + offset; + + Font font = new Font(config.swarmerFontType().toString(), config.useBoldFont() ? Font.BOLD : Font.PLAIN, config.swarmerFontSize()); + FontRenderContext frc = graphics.getFontRenderContext(); + TextLayout tl = new TextLayout(text, font, frc); + Shape outline = tl.getOutline(null); + graphics.translate(x, y); + graphics.setStroke(new BasicStroke(3)); + graphics.setColor(Color.BLACK); + graphics.draw(outline); + graphics.setColor(config.swarmerFontColor()); + graphics.fill(outline); + graphics.translate(-x, -y); + } +} diff --git a/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerPanel.java b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerPanel.java new file mode 100644 index 0000000..3620475 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerPanel.java @@ -0,0 +1,182 @@ +package com.duckblade.osrs.toa.features.boss.kephri.swarmer; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.util.Comparator; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableModel; +import net.runelite.client.ui.PluginPanel; + +@Singleton +public class SwarmerPanel extends PluginPanel +{ + private static final String[] LEAKS_COLUMN_NAMES = {"Down", "Wave", "Leaks"}; + + private static final Color textColor = Color.WHITE; + private static final Color tableColor1 = new Color(0x1F1F1F); + private static final Color tableColor2 = new Color(0x2D2D2D); + + private final SwarmerDataManager swarmerDataManager; + + private final DefaultListModel<String> raidsListModel; + private final DefaultTableModel leaksTableModel; + + private String loadedRaidData; + private String selectedRaid; + + @Inject + SwarmerPanel(SwarmerDataManager swarmerDataManager) + { + super(false); + this.swarmerDataManager = swarmerDataManager; + + Font tableTitleFont = new Font(SwarmerFonts.REGULAR.toString(), Font.PLAIN, 18); + Font tableFont = new Font(SwarmerFonts.VERDANA.toString(), Font.PLAIN, 12); + + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + add(Box.createVerticalStrut(20)); + + JPanel recentRaidsPanel = new JPanel(); + recentRaidsPanel.setLayout(new BoxLayout(recentRaidsPanel, BoxLayout.Y_AXIS)); + + JLabel recentRaidsLabel = new JLabel("Recent Raids"); + recentRaidsLabel.setForeground(textColor); + recentRaidsLabel.setFont(tableTitleFont); + recentRaidsLabel.setHorizontalAlignment(SwingConstants.CENTER); + recentRaidsLabel.setAlignmentX(CENTER_ALIGNMENT); + recentRaidsPanel.add(recentRaidsLabel); + recentRaidsPanel.add(Box.createVerticalStrut(5)); + + raidsListModel = new DefaultListModel<>(); + JList<String> raidsList = new JList<>(raidsListModel); + raidsList.setBackground(tableColor1); + raidsList.setForeground(textColor); + raidsList.setFont(tableFont); + raidsList.setSelectionBackground(tableColor2); + raidsList.setSelectionForeground(textColor); + raidsList.addListSelectionListener(e -> + { + if (!e.getValueIsAdjusting()) + { + String selectedRaid = raidsList.getSelectedValue(); + if (selectedRaid != null) + { + this.selectedRaid = selectedRaid; + loadRaidData(selectedRaid); + } + } + }); + raidsList.setCellRenderer(new DefaultListCellRenderer() + { + @Override + public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) + { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + label.setHorizontalAlignment(SwingConstants.CENTER); + label.setBackground(value.equals(selectedRaid) ? tableColor2 : tableColor1); + label.setPreferredSize(new Dimension(label.getPreferredSize().width, 25)); + return label; + } + }); + + recentRaidsPanel.add(new JScrollPane(raidsList)); + add(recentRaidsPanel); + add(Box.createVerticalStrut(20)); + + JPanel leaksPanel = new JPanel(); + leaksPanel.setLayout(new BoxLayout(leaksPanel, BoxLayout.Y_AXIS)); + + JLabel leaksLabel = new JLabel("Leaks"); + leaksLabel.setForeground(textColor); + leaksLabel.setFont(tableTitleFont); + leaksLabel.setHorizontalAlignment(SwingConstants.CENTER); + leaksLabel.setAlignmentX(CENTER_ALIGNMENT); + leaksPanel.add(leaksLabel); + leaksPanel.add(Box.createVerticalStrut(5)); + + leaksTableModel = new DefaultTableModel() + { + @Override + public boolean isCellEditable(int row, int column) + { + return false; + } + }; + JTable leaksTable = new JTable(leaksTableModel); + leaksTable.setRowSelectionAllowed(false); + leaksTable.setColumnSelectionAllowed(false); + leaksTable.setCellSelectionEnabled(false); + leaksTable.setBackground(tableColor1); + leaksTable.setForeground(textColor); + leaksTable.setGridColor(tableColor1); + leaksTable.setFont(tableFont); + + DefaultTableCellRenderer leaksCellRenderer = new DefaultTableCellRenderer() + { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) + { + Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + c.setBackground(row % 2 == 0 ? tableColor1 : tableColor2); + c.setForeground(textColor); + return c; + } + }; + leaksCellRenderer.setHorizontalAlignment(SwingConstants.CENTER); + leaksTable.setDefaultRenderer(Object.class, leaksCellRenderer); + + JScrollPane leaksScrollPane = new JScrollPane(leaksTable); + leaksScrollPane.setPreferredSize(new Dimension(100, 400)); + + leaksPanel.add(leaksScrollPane); + add(leaksPanel); + add(Box.createVerticalStrut(20)); + } + + public void loadRaidData(String raid) + { + if (raid == null || raid.equals(loadedRaidData)) + { + return; + } + + // set this early to prevent multi-clicks from possibly beating out the execution of the read. + loadedRaidData = raid; + swarmerDataManager.getRaidData(raid) + .thenAccept(raidDataList -> SwingUtilities.invokeLater(() -> + { + Object[][] newData = raidDataList.stream() + .sorted(Comparator.comparing(SwarmerRoomData::getDown) + .thenComparing(SwarmerRoomData::getWave)) + .map(row -> new Object[]{row.getDown(), row.getWave(), row.getLeaks()}) + .toArray(Object[][]::new); + leaksTableModel.setDataVector(newData, LEAKS_COLUMN_NAMES); + })); + } + + public void updateRecentRaids() + { + swarmerDataManager.getRaidList() + .thenAccept(raids -> SwingUtilities.invokeLater(() -> + { + raidsListModel.clear(); + raidsListModel.addAll(raids); + })); + } + +} diff --git a/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerPanelManager.java b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerPanelManager.java new file mode 100644 index 0000000..c633f21 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerPanelManager.java @@ -0,0 +1,73 @@ +package com.duckblade.osrs.toa.features.boss.kephri.swarmer; + +import com.duckblade.osrs.toa.TombsOfAmascutConfig; +import com.duckblade.osrs.toa.module.PluginLifecycleComponent; +import com.duckblade.osrs.toa.util.RaidState; +import java.awt.image.BufferedImage; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.swing.SwingUtilities; +import lombok.RequiredArgsConstructor; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.util.ImageUtil; + +@Singleton +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class SwarmerPanelManager implements PluginLifecycleComponent +{ + + public enum PanelMode + { + ALWAYS, + AT_TOA, + NEVER, + ; + } + + private static final BufferedImage PANEL_ICON = ImageUtil.loadImageResource(SwarmerPanelManager.class, "icon.png"); + + private final ClientToolbar clientToolbar; + private final SwarmerPanel swarmerPanel; + + private NavigationButton navButton; + + @Override + public boolean isEnabled(TombsOfAmascutConfig config, RaidState raidState) + { + switch (config.swarmerSidePanel()) + { + case ALWAYS: + return true; + + case AT_TOA: + return raidState.isInLobby() || raidState.isInRaid(); + + case NEVER: + default: + return false; + } + } + + @Override + public void startUp() + { + SwingUtilities.invokeLater(() -> + { + navButton = NavigationButton.builder() + .tooltip("Swarmer") + .icon(PANEL_ICON) + .priority(999) + .panel(swarmerPanel) + .build(); + clientToolbar.addNavigation(navButton); + swarmerPanel.updateRecentRaids(); + }); + } + + @Override + public void shutDown() + { + clientToolbar.removeNavigation(navButton); + } +} diff --git a/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerRoomData.java b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerRoomData.java new file mode 100644 index 0000000..caa1b32 --- /dev/null +++ b/src/main/java/com/duckblade/osrs/toa/features/boss/kephri/swarmer/SwarmerRoomData.java @@ -0,0 +1,15 @@ +package com.duckblade.osrs.toa.features.boss.kephri.swarmer; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class SwarmerRoomData +{ + + private int down; + private int wave; + private int leaks; + +} diff --git a/src/main/java/com/duckblade/osrs/toa/module/TombsOfAmascutModule.java b/src/main/java/com/duckblade/osrs/toa/module/TombsOfAmascutModule.java index 5d944a7..2e0b19a 100644 --- a/src/main/java/com/duckblade/osrs/toa/module/TombsOfAmascutModule.java +++ b/src/main/java/com/duckblade/osrs/toa/module/TombsOfAmascutModule.java @@ -10,6 +10,9 @@ import com.duckblade.osrs.toa.features.boss.akkha.AkkhaShadowHealthOverlay; import com.duckblade.osrs.toa.features.apmeken.ApmekenBaboonIndicator; import com.duckblade.osrs.toa.features.apmeken.ApmekenBaboonIndicatorOverlay; +import com.duckblade.osrs.toa.features.boss.kephri.swarmer.SwarmerDataManager; +import com.duckblade.osrs.toa.features.boss.kephri.swarmer.SwarmerOverlay; +import com.duckblade.osrs.toa.features.boss.kephri.swarmer.SwarmerPanelManager; import com.duckblade.osrs.toa.features.het.pickaxe.DepositPickaxeOverlay; import com.duckblade.osrs.toa.features.het.pickaxe.DepositPickaxePreventEntry; import com.duckblade.osrs.toa.features.het.solver.HetSolver; @@ -96,6 +99,9 @@ protected void configure() lifecycleComponents.addBinding().to(SmellingSaltsCooldown.class); lifecycleComponents.addBinding().to(SplitsOverlay.class); lifecycleComponents.addBinding().to(SplitsTracker.class); + lifecycleComponents.addBinding().to(SwarmerOverlay.class); + lifecycleComponents.addBinding().to(SwarmerPanelManager.class); + lifecycleComponents.addBinding().to(SwarmerDataManager.class); lifecycleComponents.addBinding().to(TargetTimeManager.class); lifecycleComponents.addBinding().to(UpdateNotifier.class); } diff --git a/src/main/resources/com/duckblade/osrs/toa/features/boss/kephri/swarmer/icon.png b/src/main/resources/com/duckblade/osrs/toa/features/boss/kephri/swarmer/icon.png new file mode 100644 index 0000000..bd3b365 Binary files /dev/null and b/src/main/resources/com/duckblade/osrs/toa/features/boss/kephri/swarmer/icon.png differ