Files
Runelite-EssentialsPlus/src/main/java/com/example/essentialsplus/loot/ui/LootSummaryCard.java
T
2026-09-18 17:18:57 +02:00

387 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<LootRecord> 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();
}
}