package com.example.essentialsplus.loot.ui; import com.example.essentialsplus.loot.LootRecord; import com.example.essentialsplus.loot.LootTrackerStore; import com.example.essentialsplus.loot.TrackedLootItem; import com.example.essentialsplus.ui.TimeFilter; import com.example.essentialsplus.ui.Ui; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Comparator; import java.util.List; import javax.swing.JComponent; import javax.swing.SwingUtilities; import net.runelite.client.game.ItemManager; import net.runelite.client.ui.FontManager; import net.runelite.client.util.QuantityFormatter; /** * Clean entry card for Loot Tracker on the Essentials Plus Overview panel. * Height strictly bounded to 110-120px to prevent layout bloat. */ public class LootSummaryCard extends JComponent { private final LootTrackerStore store; private final ItemManager itemManager; private final Runnable onClick; private final TimeFilter timeFilter; private int sourceCount = 0; private boolean hover = false; private long totalValue = 0; private int totalKills = 0; private String topSource = ""; private long topSourceVal = 0; /** 0 = total value, 1 = latest drop, 2 = top source (overview right-click menu) */ private int mode; private String latestSource = ""; private long latestVal; private long latestAt; private int latestKills; /** live fight shown instead of the loot figures (overview right-click options) */ private com.example.essentialsplus.loot.gear.FightSnapshot fight = com.example.essentialsplus.loot.gear.FightSnapshot.LOGGED_OUT; private boolean showTarget = true; private boolean keepLast; private boolean showHp = true; private boolean showDps = true; private boolean showSession = true; private boolean showKills = true; public LootSummaryCard(LootTrackerStore store, ItemManager itemManager, TimeFilter timeFilter, Runnable onClick) { this.store = store; this.timeFilter = timeFilter; this.itemManager = itemManager; this.onClick = onClick; setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { hover = true; repaint(); } @Override public void mouseExited(MouseEvent e) { hover = false; repaint(); } @Override public void mousePressed(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e) && onClick != null) { onClick.run(); } } }); store.addChangeListener(this::refresh); timeFilter.addListener(this::refresh); refresh(); } public void refresh() { List records = store.getRecords(timeFilter); totalValue = LootTrackerStore.totalValue(records); totalKills = LootTrackerStore.totalKills(records); sourceCount = records.size(); LootRecord top = records.stream() .max(Comparator.comparingLong(LootRecord::getTotalValue)) .orElse(null); LootRecord latest = records.stream() .filter(r -> r.getTotalValue() > 0) .max(Comparator.comparingLong(LootRecord::getLastSeen)) .orElse(null); latestSource = latest == null ? "" : latest.getName(); latestVal = latest == null ? 0 : latest.getTotalValue(); latestAt = latest == null ? 0 : latest.getLastSeen(); latestKills = latest == null ? 0 : latest.getKills(); if (top != null && top.getTotalValue() > 0) { topSource = top.getName(); topSourceVal = top.getTotalValue(); } else { topSource = ""; topSourceVal = 0; } repaint(); } public void setFight(com.example.essentialsplus.loot.gear.FightSnapshot snap) { boolean was = fightShown(); fight = snap; if (was || fightShown()) { repaint(); } } public void setFightOptions(boolean showTarget, boolean keepLast, boolean showHp, boolean showDps, boolean showSession, boolean showKills) { this.showTarget = showTarget; this.keepLast = keepLast; this.showHp = showHp; this.showDps = showDps; this.showSession = showSession; this.showKills = showKills; repaint(); } private boolean fightShown() { com.example.essentialsplus.loot.gear.FightSnapshot.Target t = fight.loggedIn ? fight.target : null; return showTarget && t != null && (t.live || keepLast); } public void setMode(int mode) { this.mode = mode; repaint(); } private static String ago(long t) { long d = System.currentTimeMillis() - t; return d < Ui.MINUTE ? "just now" : d < Ui.HOUR ? d / Ui.MINUTE + "m ago" : d < Ui.DAY ? d / Ui.HOUR + "h ago" : d / Ui.DAY + "d ago"; } @Override public Dimension getPreferredSize() { return new Dimension(net.runelite.client.ui.PluginPanel.PANEL_WIDTH - 16, 120); } @Override public Dimension getMaximumSize() { return new Dimension(Integer.MAX_VALUE, 130); } @Override public Dimension getMinimumSize() { return new Dimension(10, 110); } /** Current target: name, badge, health bar, own max hit / dps, fight time and damage. */ private void paintFight(Graphics2D g, int pad, int right, FontMetrics headerFm) { com.example.essentialsplus.loot.gear.FightSnapshot.Target t = fight.target; Font bold = FontManager.getRunescapeBoldFont(); g.setFont(FontManager.getRunescapeSmallFont()); FontMetrics sfm = g.getFontMetrics(); String badge = t.dead ? "DEAD" : t.live ? "FIGHTING" : "LAST TARGET"; Color bc = t.dead ? Ui.MUTED : t.live ? Ui.DOWN : Ui.FAINT; int bw = sfm.stringWidth(badge) + 6; int bx = right - 14 - bw; g.setColor(Ui.alpha(bc, 50)); g.fillRoundRect(bx, 6, bw, sfm.getAscent() + 3, 4, 4); g.setColor(bc); g.drawString(badge, bx + 3, 7 + sfm.getAscent()); int y = 24; g.setFont(bold.deriveFont(18f)); FontMetrics nfm = g.getFontMetrics(); g.setColor(Color.WHITE); String name = t.name + " (" + t.combatLevel + ")"; g.drawString(Ui.ellipsize(nfm, name, right - pad), pad, y + nfm.getAscent()); y += nfm.getHeight() + 2; if (showHp) { int barW = right - pad; g.setColor(new Color(60, 20, 20)); g.fillRoundRect(pad, y, barW, 12, 4, 4); double ratio = t.healthRatio >= 0 && t.healthScale > 0 ? (double) t.healthRatio / t.healthScale : t.dead ? 0 : 1; g.setColor(t.live ? new Color(40, 170, 70) : new Color(60, 110, 70)); g.fillRoundRect(pad, y, (int) Math.round(barW * ratio), 12, 4, 4); String hp = t.monster != null && t.monster.hitpoints > 0 ? (int) Math.round(t.monster.hitpoints * ratio) + " / " + t.monster.hitpoints : String.format(java.util.Locale.US, "%.0f%%", ratio * 100); g.setFont(FontManager.getRunescapeSmallFont()); g.setColor(Color.WHITE); g.drawString(hp, pad + (barW - sfm.stringWidth(hp)) / 2, y + 10); y += 16; } g.setFont(FontManager.getRunescapeSmallFont()); if (showDps) { com.example.essentialsplus.loot.gear.DpsCalc.Result r = com.example.essentialsplus.loot.gear.DpsCalc.calc(fight.worn, fight.setup, t.calcMonster(), com.example.essentialsplus.loot.gear.DpsCalc.choiceFor(fight.worn, fight.worn, fight.setup)); y += sfm.getAscent() + 2; int x = pad; x = pair(g, sfm, x, y, "Max ", String.valueOf(r.maxHit), Ui.GOLD); x = pair(g, sfm, x, y, " DPS ", String.format(java.util.Locale.US, "%.2f", r.dps), Ui.UP); pair(g, sfm, x, y, " Hit ", String.format(java.util.Locale.US, "%.0f%%", r.hitChance * 100), Ui.TEXT); } if (showSession && fight.session != null) { com.example.essentialsplus.loot.gear.FightSnapshot.Session se = fight.session; y += sfm.getAscent() + 4; int sec = (int) Math.round(se.durationTicks * 0.6); int x = pad; x = pair(g, sfm, x, y, "", sec >= 60 ? sec / 60 + "m " + sec % 60 + "s" : sec + "s", Ui.TEXT); x = pair(g, sfm, x, y, " Dealt ", String.valueOf(se.damageDealt), Ui.UP); pair(g, sfm, x, y, " Taken ", String.valueOf(se.damageTaken), se.damageTaken > 0 ? Ui.DOWN : Ui.TEXT); } else if (showKills && totalKills > 0) { y += sfm.getAscent() + 4; g.setColor(Ui.FAINT); g.drawString(Ui.gp(totalValue) + " loot • " + QuantityFormatter.formatNumber(totalKills) + " kills • " + timeFilter.getLabel(), pad, y); } } private static int pair(Graphics2D g, FontMetrics fm, int x, int y, String label, String value, Color color) { g.setColor(Ui.MUTED); g.drawString(label, x, y); x += fm.stringWidth(label); g.setColor(color); g.drawString(value, x, y); return x + fm.stringWidth(value); } @Override protected void paintComponent(Graphics g0) { Graphics2D g = Ui.prepare(g0); int w = getWidth(); int h = getHeight(); g.setColor(hover ? Ui.CARD_HOVER : Ui.CARD); g.fillRoundRect(0, 0, w, h, 8, 8); int pad = 10; int right = w - pad; Font bold = FontManager.getRunescapeBoldFont(); Font small = FontManager.getRunescapeSmallFont(); // Header: "Loot Tracker" + chevron › g.setFont(bold); FontMetrics fm = g.getFontMetrics(); g.setColor(Ui.ACCENT); g.drawString("Fights & Loot", pad, 6 + fm.getAscent()); g.setFont(bold.deriveFont(14f)); g.setColor(hover ? Ui.ACCENT : Ui.FAINT); Ui.drawRight(g, "›", right, 6 + fm.getAscent()); if (fightShown()) { paintFight(g, pad, right, fm); Ui.paintDragOutline(g, this); g.dispose(); return; } if (totalKills > 0 && showKills) { g.setFont(small); g.setColor(Ui.MUTED); String killsText = QuantityFormatter.formatNumber(totalKills) + " kills • " + timeFilter.getLabel(); FontMetrics sfm = g.getFontMetrics(); int titleW = fm.stringWidth("Fights & Loot"); int killsW = sfm.stringWidth(killsText); int maxKillsX = right - 14; if (maxKillsX - killsW > pad + titleW + 8) { Ui.drawRight(g, killsText, maxKillsX, 6 + fm.getAscent()); } } if (mode != 0 && totalKills > 0) { boolean latestMode = mode == 1; String src = latestMode ? latestSource : topSource; long val = latestMode ? latestVal : topSourceVal; g.setFont(bold.deriveFont(26f)); FontMetrics bfm = g.getFontMetrics(); int valueBase = 22 + bfm.getAscent(); g.setColor(val > 0 ? Ui.GOLD : Ui.FAINT); g.drawString(Ui.gp(val), pad, valueBase); g.setFont(small); FontMetrics sfm = g.getFontMetrics(); int line1 = valueBase + sfm.getAscent() + 4; g.setColor(Ui.MUTED); String prefix = latestMode ? "Latest: " : "Top: "; g.drawString(prefix, pad, line1); g.setColor(Color.WHITE); g.drawString(src.isEmpty() ? "-" : src, pad + sfm.stringWidth(prefix), line1); int line2 = line1 + sfm.getAscent() + 4; g.setColor(Ui.FAINT); g.drawString(latestMode && latestAt > 0 ? ago(latestAt) + " • " + latestKills + " kills" : "Total " + Ui.gp(totalValue) + " • " + timeFilter.getLabel(), pad, line2); Ui.paintDragOutline(g, this); g.dispose(); return; } // Big Value Font big = bold.deriveFont(26f); g.setFont(big); FontMetrics bfm = g.getFontMetrics(); int valueBase = 22 + bfm.getAscent(); g.setColor(totalValue > 0 ? Ui.GOLD : Ui.FAINT); g.drawString(totalValue > 0 ? Ui.gp(totalValue) : "0 gp", pad, valueBase); // Subtitle & highlight g.setFont(small); FontMetrics sfm = g.getFontMetrics(); int line1 = valueBase + sfm.getAscent() + 4; if (totalKills == 0) { g.setColor(Ui.FAINT); g.drawString((timeFilter.start() > 0 ? "No loot " + timeFilter.inLabel() : "No loot recorded yet") + " • Click to open", pad, line1); } else { g.setColor(Ui.TEXT); String summaryDesc = "Tracked drops from " + sourceCount + " sources"; g.drawString(summaryDesc, pad, line1); if (!topSource.isEmpty()) { int line2 = line1 + sfm.getAscent() + 4; g.setColor(Ui.MUTED); g.drawString("Top: ", pad, line2); int topW = sfm.stringWidth("Top: "); g.setColor(Color.WHITE); g.drawString(topSource, pad + topW, line2); int nameW = sfm.stringWidth(topSource); g.setColor(Ui.GOLD); g.drawString(" (" + Ui.gp(topSourceVal) + ")", pad + topW + nameW, line2); } } Ui.paintDragOutline(g, this); g.dispose(); } }