chore: init

This commit is contained in:
Hikyu
2026-09-18 17:18:57 +02:00
commit 15463c2e3c
200 changed files with 51032 additions and 0 deletions
@@ -0,0 +1,386 @@
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();
}
}