387 lines
12 KiB
Java
387 lines
12 KiB
Java
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();
|
||
}
|
||
}
|