1008 lines
31 KiB
Java
1008 lines
31 KiB
Java
package com.example.essentialsplus.ui;
|
|||
|
|
|
||
|
|
import com.example.essentialsplus.EssentialsPlusPlugin;
|
||
|
|
import com.example.essentialsplus.skills.SkillGoals;
|
||
|
|
import com.example.essentialsplus.skills.SkillHistory;
|
||
|
|
import com.example.essentialsplus.skills.SkillStore;
|
||
|
|
import com.example.essentialsplus.skills.XpSample;
|
||
|
|
import java.awt.BorderLayout;
|
||
|
|
import java.awt.Color;
|
||
|
|
import java.awt.Cursor;
|
||
|
|
import java.awt.Dimension;
|
||
|
|
import java.awt.FontMetrics;
|
||
|
|
import java.awt.Graphics;
|
||
|
|
import java.awt.Graphics2D;
|
||
|
|
import java.awt.event.MouseAdapter;
|
||
|
|
import java.awt.event.MouseEvent;
|
||
|
|
import java.awt.image.BufferedImage;
|
||
|
|
import java.time.Instant;
|
||
|
|
import java.time.ZoneId;
|
||
|
|
import java.time.format.DateTimeFormatter;
|
||
|
|
import java.util.ArrayList;
|
||
|
|
import java.util.EnumMap;
|
||
|
|
import java.util.List;
|
||
|
|
import java.util.Locale;
|
||
|
|
import java.util.Map;
|
||
|
|
import java.util.Objects;
|
||
|
|
import javax.swing.BorderFactory;
|
||
|
|
import javax.swing.JButton;
|
||
|
|
import javax.swing.JCheckBox;
|
||
|
|
import javax.swing.JCheckBoxMenuItem;
|
||
|
|
import javax.swing.JMenuItem;
|
||
|
|
import javax.swing.JPopupMenu;
|
||
|
|
import javax.swing.JComboBox;
|
||
|
|
import javax.swing.JComponent;
|
||
|
|
import javax.swing.JLabel;
|
||
|
|
import javax.swing.JPanel;
|
||
|
|
import javax.swing.JSpinner;
|
||
|
|
import javax.swing.SpinnerNumberModel;
|
||
|
|
import javax.swing.SwingUtilities;
|
||
|
|
import javax.swing.Timer;
|
||
|
|
import net.runelite.api.Experience;
|
||
|
|
import net.runelite.api.Skill;
|
||
|
|
import net.runelite.client.ui.DynamicGridLayout;
|
||
|
|
import net.runelite.client.ui.FontManager;
|
||
|
|
import net.runelite.client.ui.PluginPanel;
|
||
|
|
import net.runelite.client.util.ImageUtil;
|
||
|
|
|
||
|
|
/** Skills tab: xp gained per skill over the global time range, with charts and level-up history. */
|
||
|
|
public class SkillsPanel extends PluginPanel
|
||
|
|
{
|
||
|
|
private static final int LEVEL_UP_LIMIT = 15;
|
||
|
|
private static final DateTimeFormatter WHEN = DateTimeFormatter.ofPattern("d MMM HH:mm", Locale.ENGLISH);
|
||
|
|
|
||
|
|
private final EssentialsPlusPlugin plugin;
|
||
|
|
private final SkillStore store;
|
||
|
|
private final TimeFilter timeFilter;
|
||
|
|
private final Runnable rangeListener = this::refreshLater;
|
||
|
|
private final Timer coalesce;
|
||
|
|
private final Timer ticker;
|
||
|
|
private final Map<Skill, BufferedImage> icons = new EnumMap<>(Skill.class);
|
||
|
|
|
||
|
|
private final SkillsCard card = new SkillsCard(null);
|
||
|
|
private final JComboBox<Choice> characterBox = new JComboBox<>();
|
||
|
|
private final JLabel chartTitle = new JLabel();
|
||
|
|
private final JLabel showAll = new JLabel("Show all");
|
||
|
|
private final XpChart cumulative = new XpChart(XpChart.Mode.CUMULATIVE, 150);
|
||
|
|
private final XpChart perPeriod = new XpChart(XpChart.Mode.BARS, 100);
|
||
|
|
private final StatsView stats = new StatsView(2);
|
||
|
|
private final JPanel skillList = new JPanel(new DynamicGridLayout(0, 1, 0, 2));
|
||
|
|
private final JPanel levelList = new JPanel(new DynamicGridLayout(0, 1, 0, 2));
|
||
|
|
private final JLabel levelEmpty = new JLabel();
|
||
|
|
private final JCheckBox hudBox = new JCheckBox("Show xp HUD in-game");
|
||
|
|
private final SkillRow[] rows = new SkillRow[SkillHistory.SKILLS.length];
|
||
|
|
private final List<Integer> rowOrder = new ArrayList<>();
|
||
|
|
|
||
|
|
private String selectedKey;
|
||
|
|
/** character whose skills are shown (goals belong to it) */
|
||
|
|
private String shownKey;
|
||
|
|
private int selectedSkill = -1;
|
||
|
|
private boolean updatingCombo;
|
||
|
|
|
||
|
|
public SkillsPanel(EssentialsPlusPlugin plugin, Runnable onBack)
|
||
|
|
{
|
||
|
|
super(true);
|
||
|
|
this.plugin = plugin;
|
||
|
|
this.store = plugin.getSkillStore();
|
||
|
|
this.timeFilter = plugin.getTimeFilter();
|
||
|
|
|
||
|
|
setBackground(Ui.BG);
|
||
|
|
setLayout(new DynamicGridLayout(0, 1, 0, 6));
|
||
|
|
|
||
|
|
JPanel backRow = new JPanel(new BorderLayout(6, 0));
|
||
|
|
backRow.setOpaque(false);
|
||
|
|
backRow.setBorder(BorderFactory.createEmptyBorder(0, 0, 4, 0));
|
||
|
|
JButton backBtn = button("← Overview", onBack);
|
||
|
|
backBtn.setForeground(Ui.ACCENT);
|
||
|
|
backRow.add(backBtn, BorderLayout.WEST);
|
||
|
|
JLabel titleLabel = new JLabel("Skills");
|
||
|
|
titleLabel.setFont(FontManager.getRunescapeBoldFont());
|
||
|
|
titleLabel.setForeground(Ui.ACCENT);
|
||
|
|
titleLabel.setHorizontalAlignment(JLabel.RIGHT);
|
||
|
|
backRow.add(titleLabel, BorderLayout.CENTER);
|
||
|
|
add(backRow);
|
||
|
|
|
||
|
|
characterBox.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
characterBox.setBackground(Ui.CARD);
|
||
|
|
characterBox.setFocusable(false);
|
||
|
|
characterBox.addActionListener(e ->
|
||
|
|
{
|
||
|
|
Choice c = (Choice) characterBox.getSelectedItem();
|
||
|
|
if (!updatingCombo && c != null)
|
||
|
|
{
|
||
|
|
selectedKey = c.key;
|
||
|
|
refresh();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
characterBox.setVisible(false);
|
||
|
|
add(characterBox);
|
||
|
|
|
||
|
|
card.addMouseListener(new MouseAdapter()
|
||
|
|
{
|
||
|
|
@Override
|
||
|
|
public void mousePressed(MouseEvent e)
|
||
|
|
{
|
||
|
|
maybePopup(e);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void mouseReleased(MouseEvent e)
|
||
|
|
{
|
||
|
|
maybePopup(e);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void maybePopup(MouseEvent e)
|
||
|
|
{
|
||
|
|
if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))
|
||
|
|
{
|
||
|
|
JPopupMenu menu = new JPopupMenu();
|
||
|
|
JMenuItem reset = new JMenuItem("Reset Hourly");
|
||
|
|
reset.addActionListener(ev ->
|
||
|
|
{
|
||
|
|
plugin.resetSkillsHourly(shownKey);
|
||
|
|
refreshLater();
|
||
|
|
});
|
||
|
|
menu.add(reset);
|
||
|
|
menu.show(card, e.getX(), e.getY());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
add(card);
|
||
|
|
|
||
|
|
hudBox.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
hudBox.setForeground(Ui.TEXT);
|
||
|
|
hudBox.setOpaque(false);
|
||
|
|
hudBox.setFocusPainted(false);
|
||
|
|
hudBox.setToolTipText("In-game overlay with the xp gained per skill in the selected time range");
|
||
|
|
hudBox.setSelected(plugin.isSkillsHudEnabled());
|
||
|
|
hudBox.addActionListener(e -> plugin.setSkillsHudEnabled(hudBox.isSelected()));
|
||
|
|
JButton[] hudOptions = new JButton[1];
|
||
|
|
hudOptions[0] = button("Options", () -> showHudOptions(hudOptions[0]));
|
||
|
|
hudOptions[0].setToolTipText("Choose what the in-game xp HUD shows");
|
||
|
|
JPanel hudRow = new JPanel(new BorderLayout(6, 0));
|
||
|
|
hudRow.setOpaque(false);
|
||
|
|
hudRow.add(hudBox, BorderLayout.CENTER);
|
||
|
|
hudRow.add(hudOptions[0], BorderLayout.EAST);
|
||
|
|
add(hudRow);
|
||
|
|
|
||
|
|
showAll.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
showAll.setForeground(Ui.ACCENT);
|
||
|
|
showAll.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||
|
|
showAll.addMouseListener(new MouseAdapter()
|
||
|
|
{
|
||
|
|
@Override
|
||
|
|
public void mousePressed(MouseEvent e)
|
||
|
|
{
|
||
|
|
selectSkill(-1);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
JPanel chartHeader = new JPanel(new BorderLayout(6, 0));
|
||
|
|
chartHeader.setOpaque(false);
|
||
|
|
chartHeader.setBorder(BorderFactory.createEmptyBorder(6, 2, 0, 2));
|
||
|
|
JLabel xpLabel = new JLabel("XP GAINED");
|
||
|
|
xpLabel.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
xpLabel.setForeground(Ui.MUTED);
|
||
|
|
chartHeader.add(xpLabel, BorderLayout.WEST);
|
||
|
|
chartTitle.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
chartTitle.setForeground(Ui.TEXT);
|
||
|
|
chartTitle.setHorizontalAlignment(JLabel.RIGHT);
|
||
|
|
chartHeader.add(chartTitle, BorderLayout.CENTER);
|
||
|
|
chartHeader.add(showAll, BorderLayout.EAST);
|
||
|
|
add(chartHeader);
|
||
|
|
add(cumulative);
|
||
|
|
add(stats);
|
||
|
|
add(WealthPanel.sectionHeader("XP PER PERIOD", null, null, null));
|
||
|
|
add(perPeriod);
|
||
|
|
|
||
|
|
add(WealthPanel.sectionHeader("SKILLS (click to chart, cog for a goal)", null, null, null));
|
||
|
|
skillList.setOpaque(false);
|
||
|
|
for (int i = 0; i < rows.length; i++)
|
||
|
|
{
|
||
|
|
rows[i] = new SkillRow(i);
|
||
|
|
}
|
||
|
|
add(skillList);
|
||
|
|
|
||
|
|
add(WealthPanel.sectionHeader("LEVEL UPS", null, null, null));
|
||
|
|
levelList.setOpaque(false);
|
||
|
|
levelEmpty.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
levelEmpty.setForeground(Ui.FAINT);
|
||
|
|
levelEmpty.setHorizontalAlignment(JLabel.CENTER);
|
||
|
|
add(levelList);
|
||
|
|
|
||
|
|
coalesce = new Timer(1000, e -> refresh());
|
||
|
|
coalesce.setRepeats(false);
|
||
|
|
ticker = new Timer(30_000, e -> refresh());
|
||
|
|
ticker.start();
|
||
|
|
timeFilter.addListener(rangeListener);
|
||
|
|
refresh();
|
||
|
|
}
|
||
|
|
|
||
|
|
public void dispose()
|
||
|
|
{
|
||
|
|
ticker.stop();
|
||
|
|
coalesce.stop();
|
||
|
|
timeFilter.removeListener(rangeListener);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Coalesces bursts of xp drops into one repaint per second. */
|
||
|
|
public void refreshLater()
|
||
|
|
{
|
||
|
|
if (!coalesce.isRunning())
|
||
|
|
{
|
||
|
|
coalesce.start();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void selectSkill(int skill)
|
||
|
|
{
|
||
|
|
selectedSkill = selectedSkill == skill ? -1 : skill;
|
||
|
|
refresh();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Resolves which character to show: explicit choice, else the logged-in one, else the latest. */
|
||
|
|
static SkillHistory resolve(SkillStore store, String preferred, String active)
|
||
|
|
{
|
||
|
|
SkillHistory h = store.get(preferred);
|
||
|
|
if (h == null || h.latest() == null)
|
||
|
|
{
|
||
|
|
h = store.get(active);
|
||
|
|
}
|
||
|
|
if (h == null || h.latest() == null)
|
||
|
|
{
|
||
|
|
List<SkillHistory> all = store.all();
|
||
|
|
h = all.isEmpty() ? null : all.get(0);
|
||
|
|
}
|
||
|
|
return h;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Fills a summary card (used by the overview too). */
|
||
|
|
static void fillCard(SkillsCard card, SkillHistory h, TimeFilter filter, String title)
|
||
|
|
{
|
||
|
|
if (h == null)
|
||
|
|
{
|
||
|
|
card.update(title, "", false, 0, 0, filter.getLabel(), "", new long[0], new long[0]);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
long start = h.start(filter);
|
||
|
|
long rateStart = h.rateStart(filter);
|
||
|
|
long[] gained = h.gained(filter);
|
||
|
|
long total = 0;
|
||
|
|
for (long g : gained)
|
||
|
|
{
|
||
|
|
total += g;
|
||
|
|
}
|
||
|
|
int levels = 0;
|
||
|
|
for (SkillHistory.LevelUp lu : h.levelUps(start))
|
||
|
|
{
|
||
|
|
levels += gained[lu.skill] > 0 ? lu.to - lu.from : 0;
|
||
|
|
}
|
||
|
|
long rateGained = h.gainedTotalSince(rateStart);
|
||
|
|
long active = h.activeMillis(rateStart, -1);
|
||
|
|
String xph = active >= Ui.MINUTE && rateGained > 0 ? Ui.gp(rateGained * Ui.HOUR / active) + " xp/h" : "";
|
||
|
|
|
||
|
|
List<XpSample> samples = h.range(start);
|
||
|
|
long[] t = new long[samples.size() + 1];
|
||
|
|
long[] v = new long[samples.size() + 1];
|
||
|
|
long base = samples.isEmpty() ? 0 : samples.get(0).total();
|
||
|
|
for (int i = 0; i < samples.size(); i++)
|
||
|
|
{
|
||
|
|
t[i] = Math.max(samples.get(i).t, start);
|
||
|
|
v[i] = samples.get(i).total() - base;
|
||
|
|
}
|
||
|
|
if (samples.isEmpty())
|
||
|
|
{
|
||
|
|
t = new long[0];
|
||
|
|
v = new long[0];
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
t[samples.size()] = System.currentTimeMillis();
|
||
|
|
v[samples.size()] = v[samples.size() - 1];
|
||
|
|
}
|
||
|
|
card.update(title, h.name == null ? "" : h.name, true, total, levels, filter.getLabel(), xph, t, v);
|
||
|
|
XpSample latest = h.latest();
|
||
|
|
int totalLevel = 0;
|
||
|
|
if (latest != null)
|
||
|
|
{
|
||
|
|
for (int s = 0; s < SkillHistory.SKILLS.length; s++)
|
||
|
|
{
|
||
|
|
totalLevel += SkillHistory.level(latest.get(s));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
card.setTotals(latest == null ? 0 : latest.total(), totalLevel);
|
||
|
|
card.setSkillGains(gained);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void refresh()
|
||
|
|
{
|
||
|
|
long now = System.currentTimeMillis();
|
||
|
|
String active = plugin.getSkillKey();
|
||
|
|
SkillHistory h = resolve(store, selectedKey, active);
|
||
|
|
updateCombo(h);
|
||
|
|
hudBox.setSelected(plugin.isSkillsHudEnabled());
|
||
|
|
|
||
|
|
shownKey = h == null ? null : h.key;
|
||
|
|
fillCard(card, h, timeFilter, "Skills");
|
||
|
|
String label = timeFilter.getLabel();
|
||
|
|
if (h == null)
|
||
|
|
{
|
||
|
|
chartTitle.setText("");
|
||
|
|
showAll.setVisible(false);
|
||
|
|
cumulative.setData(null, null, -1, now - Ui.HOUR, now, Ui.GOLD, "Log in to start tracking xp");
|
||
|
|
perPeriod.setData(null, null, -1, now - Ui.HOUR, now, Ui.GOLD, "No data yet");
|
||
|
|
stats.update(new ArrayList<>());
|
||
|
|
skillList.removeAll();
|
||
|
|
rowOrder.clear();
|
||
|
|
levelList.removeAll();
|
||
|
|
revalidate();
|
||
|
|
repaint();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
long start = h.start(timeFilter);
|
||
|
|
long[] starts = h.starts(timeFilter);
|
||
|
|
long rateStart = h.rateStart(timeFilter);
|
||
|
|
long[] rateStarts = h.rateStarts(timeFilter);
|
||
|
|
List<XpSample> samples = h.range(start);
|
||
|
|
long chartStart = samples.isEmpty() ? now - Ui.HOUR : Math.max(start, samples.get(0).t);
|
||
|
|
if (timeFilter.getRange() != TimeRange.ALL)
|
||
|
|
{
|
||
|
|
chartStart = start;
|
||
|
|
}
|
||
|
|
long[] gained = h.gained(timeFilter);
|
||
|
|
List<SkillHistory.LevelUp> levelUps = h.levelUps(start);
|
||
|
|
levelUps.removeIf(lu -> gained[lu.skill] <= 0);
|
||
|
|
XpSample last = h.latest();
|
||
|
|
|
||
|
|
// ---- chart + stats for the selection
|
||
|
|
int sel = selectedSkill;
|
||
|
|
Color color = sel < 0 ? Ui.GOLD : skillColor(SkillHistory.SKILLS[sel]);
|
||
|
|
chartTitle.setText(sel < 0 ? "All skills" : SkillHistory.SKILLS[sel].getName());
|
||
|
|
chartTitle.setForeground(sel < 0 ? Ui.TEXT : color);
|
||
|
|
showAll.setVisible(sel >= 0);
|
||
|
|
String empty = "No xp gained " + timeFilter.inLabel();
|
||
|
|
cumulative.setData(samples, levelUps, sel, chartStart, now, color, empty);
|
||
|
|
perPeriod.setData(samples, levelUps, sel, chartStart, now, color, empty);
|
||
|
|
|
||
|
|
long total = 0;
|
||
|
|
int trained = 0;
|
||
|
|
for (long g : gained)
|
||
|
|
{
|
||
|
|
total += g;
|
||
|
|
trained += g > 0 ? 1 : 0;
|
||
|
|
}
|
||
|
|
long selGained = sel < 0 ? total : gained[sel];
|
||
|
|
long rateSelStart = sel < 0 ? rateStart : rateStarts[sel];
|
||
|
|
long selRateGained = sel < 0 ? h.gainedTotalSince(rateStart) : h.gainedSince(rateStarts[sel], sel);
|
||
|
|
long activeMs = h.activeMillis(rateSelStart, sel);
|
||
|
|
int levels = 0;
|
||
|
|
for (SkillHistory.LevelUp lu : levelUps)
|
||
|
|
{
|
||
|
|
if (sel < 0 || lu.skill == sel)
|
||
|
|
{
|
||
|
|
levels += lu.to - lu.from;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
List<StatsView.Cell> cells = new ArrayList<>();
|
||
|
|
cells.add(new StatsView.Cell("XP gained", "+" + Ui.gp(selGained), label, selGained > 0 ? Ui.UP : Ui.FAINT,
|
||
|
|
String.format(Locale.US, "%,d xp", selGained)));
|
||
|
|
cells.add(new StatsView.Cell("XP / hour", activeMs >= Ui.MINUTE && selRateGained > 0 ? Ui.gp(selRateGained * Ui.HOUR / activeMs) : "-",
|
||
|
|
"while training", Ui.TEXT, "Xp per hour of active training time"));
|
||
|
|
cells.add(new StatsView.Cell("Levels", "+" + levels, null, levels > 0 ? Ui.UP : Ui.TEXT, "Levels gained in " + label));
|
||
|
|
cells.add(new StatsView.Cell("Active", duration(activeMs), null, Ui.TEXT, "Time spent gaining xp (gaps under 5 minutes)"));
|
||
|
|
if (sel < 0)
|
||
|
|
{
|
||
|
|
int totalLevel = 0;
|
||
|
|
for (int i = 0; i < SkillHistory.SKILLS.length; i++)
|
||
|
|
{
|
||
|
|
totalLevel += Math.min(99, SkillHistory.level(last.get(i)));
|
||
|
|
}
|
||
|
|
cells.add(new StatsView.Cell("Skills trained", String.valueOf(trained), null, Ui.TEXT, null));
|
||
|
|
cells.add(new StatsView.Cell("Total level", String.valueOf(totalLevel), null, Ui.GOLD, null));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
int xp = last.get(sel);
|
||
|
|
int lvl = SkillHistory.level(xp);
|
||
|
|
String toNext = lvl >= Experience.MAX_VIRT_LEVEL ? "-" : Ui.gp(Experience.getXpForLevel(lvl + 1) - xp);
|
||
|
|
cells.add(new StatsView.Cell("Level", String.valueOf(lvl), Ui.gp(xp) + " xp", color, String.format(Locale.US, "%,d xp", xp)));
|
||
|
|
cells.add(new StatsView.Cell("To level " + Math.min(lvl + 1, Experience.MAX_VIRT_LEVEL), toNext, null, Ui.TEXT, null));
|
||
|
|
SkillGoals.Goal goal = goal(sel);
|
||
|
|
if (goal != null)
|
||
|
|
{
|
||
|
|
int left = goal.xpLeft(xp);
|
||
|
|
long selXph = activeMs >= Ui.MINUTE && selGained > 0 ? selGained * Ui.HOUR / activeMs : 0;
|
||
|
|
cells.add(new StatsView.Cell("Goal " + goal.level, percent(goal.progress(xp)), left == 0 ? "reached" : Ui.gp(left) + " xp left",
|
||
|
|
left == 0 ? Ui.UP : Ui.GOLD, String.format(Locale.US, "%,d / %,d xp", xp, goal.targetXp())));
|
||
|
|
cells.add(new StatsView.Cell("Time to goal", left == 0 ? "-" : selXph > 0 ? duration(left * Ui.HOUR / selXph) : "-",
|
||
|
|
selXph > 0 ? "at " + Ui.gp(selXph) + "/h" : "no xp/h yet", Ui.TEXT, null));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
stats.update(cells);
|
||
|
|
|
||
|
|
// ---- skill rows (trained skills by xp, then the rest in game order)
|
||
|
|
List<Integer> order = new ArrayList<>();
|
||
|
|
for (int i = 0; i < gained.length; i++)
|
||
|
|
{
|
||
|
|
order.add(i);
|
||
|
|
}
|
||
|
|
order.sort((a, b) -> Long.compare(gained[b], gained[a]));
|
||
|
|
long maxGain = gained.length == 0 ? 0 : gained[order.get(0)];
|
||
|
|
for (int i = 0; i < rows.length; i++)
|
||
|
|
{
|
||
|
|
int xp = last.get(i);
|
||
|
|
int from = gained[i] > 0 ? SkillHistory.level(h.at(starts[i]).get(i)) : SkillHistory.level(xp);
|
||
|
|
long skillRateStart = rateStarts[i];
|
||
|
|
long skillRateGained = h.gainedSince(skillRateStart, i);
|
||
|
|
long skillActive = skillRateGained > 0 ? h.activeMillis(skillRateStart, i) : 0;
|
||
|
|
long xph = skillActive >= Ui.MINUTE ? skillRateGained * Ui.HOUR / skillActive : 0;
|
||
|
|
rows[i].update(xp, SkillHistory.level(xp) - from, gained[i], xph, maxGain, i == sel, goal(i));
|
||
|
|
}
|
||
|
|
if (!order.equals(rowOrder))
|
||
|
|
{
|
||
|
|
rowOrder.clear();
|
||
|
|
rowOrder.addAll(order);
|
||
|
|
skillList.removeAll();
|
||
|
|
for (int i : order)
|
||
|
|
{
|
||
|
|
skillList.add(rows[i]);
|
||
|
|
}
|
||
|
|
skillList.revalidate();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- level ups, newest first
|
||
|
|
levelList.removeAll();
|
||
|
|
if (levelUps.isEmpty())
|
||
|
|
{
|
||
|
|
levelEmpty.setText("No level ups " + timeFilter.inLabel());
|
||
|
|
levelList.add(levelEmpty);
|
||
|
|
}
|
||
|
|
for (int i = levelUps.size() - 1; i >= Math.max(0, levelUps.size() - LEVEL_UP_LIMIT); i--)
|
||
|
|
{
|
||
|
|
levelList.add(new LevelUpRow(levelUps.get(i)));
|
||
|
|
}
|
||
|
|
levelList.revalidate();
|
||
|
|
|
||
|
|
revalidate();
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
private SkillGoals.Goal goal(int skill)
|
||
|
|
{
|
||
|
|
return plugin.getSkillGoals().get(shownKey, SkillHistory.SKILLS[skill].getName());
|
||
|
|
}
|
||
|
|
|
||
|
|
private static String percent(double ratio)
|
||
|
|
{
|
||
|
|
double p = ratio * 100;
|
||
|
|
return (p >= 100 ? "100" : String.format(Locale.US, "%.1f", p)) + "%";
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Goal editor for one skill: target level with quick picks, set and clear. */
|
||
|
|
private void showGoalEditor(int skill, JComponent anchor, int x, int y)
|
||
|
|
{
|
||
|
|
if (shownKey == null)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
Skill s = SkillHistory.SKILLS[skill];
|
||
|
|
SkillHistory h = store.get(shownKey);
|
||
|
|
XpSample last = h == null ? null : h.latest();
|
||
|
|
int xp = last == null ? 0 : last.get(skill);
|
||
|
|
int level = SkillHistory.level(xp);
|
||
|
|
SkillGoals.Goal current = goal(skill);
|
||
|
|
int max = Experience.MAX_VIRT_LEVEL;
|
||
|
|
int initial = current != null ? current.level : level < 99 ? nextMilestone(level) : level + 1;
|
||
|
|
|
||
|
|
JPopupMenu menu = new JPopupMenu();
|
||
|
|
JPanel panel = new JPanel(new DynamicGridLayout(0, 1, 0, 6));
|
||
|
|
panel.setBackground(Ui.CARD);
|
||
|
|
panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||
|
|
|
||
|
|
JLabel title = new JLabel(s.getName() + " goal");
|
||
|
|
title.setFont(FontManager.getRunescapeBoldFont());
|
||
|
|
title.setForeground(skillColor(s));
|
||
|
|
panel.add(title);
|
||
|
|
JLabel now = new JLabel(String.format(Locale.US, "Now level %d (%,d xp)", level, xp));
|
||
|
|
now.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
now.setForeground(Ui.MUTED);
|
||
|
|
panel.add(now);
|
||
|
|
|
||
|
|
SpinnerNumberModel model = new SpinnerNumberModel(Math.max(2, Math.min(max, initial)), 2, max, 1);
|
||
|
|
JSpinner spinner = new JSpinner(model);
|
||
|
|
spinner.setFont(FontManager.getRunescapeFont());
|
||
|
|
JLabel preview = new JLabel();
|
||
|
|
preview.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
preview.setForeground(Ui.TEXT);
|
||
|
|
Runnable updatePreview = () ->
|
||
|
|
{
|
||
|
|
int target = Experience.getXpForLevel((Integer) model.getValue());
|
||
|
|
int left = Math.max(0, target - xp);
|
||
|
|
preview.setText(left == 0 ? "Already reached" : String.format(Locale.US, "%,d xp needed", left));
|
||
|
|
};
|
||
|
|
spinner.addChangeListener(e -> updatePreview.run());
|
||
|
|
updatePreview.run();
|
||
|
|
JPanel levelRow = new JPanel(new BorderLayout(6, 0));
|
||
|
|
levelRow.setOpaque(false);
|
||
|
|
JLabel lvLabel = new JLabel("Target level");
|
||
|
|
lvLabel.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
lvLabel.setForeground(Ui.TEXT);
|
||
|
|
levelRow.add(lvLabel, BorderLayout.WEST);
|
||
|
|
levelRow.add(spinner, BorderLayout.CENTER);
|
||
|
|
panel.add(levelRow);
|
||
|
|
|
||
|
|
JPanel quick = new JPanel(new java.awt.GridLayout(1, 4, 3, 0));
|
||
|
|
quick.setOpaque(false);
|
||
|
|
for (int[] pick : new int[][]{{1, 0}, {5, 0}, {0, 70}, {0, 99}})
|
||
|
|
{
|
||
|
|
String label = pick[0] > 0 ? "+" + pick[0] : String.valueOf(pick[1]);
|
||
|
|
quick.add(button(label, () -> model.setValue(Math.max(2, Math.min(max, pick[0] > 0 ? level + pick[0] : pick[1])))));
|
||
|
|
}
|
||
|
|
panel.add(quick);
|
||
|
|
panel.add(preview);
|
||
|
|
|
||
|
|
JPanel actions = new JPanel(new java.awt.GridLayout(1, 2, 6, 0));
|
||
|
|
actions.setOpaque(false);
|
||
|
|
JButton set = button("Set goal", () ->
|
||
|
|
{
|
||
|
|
plugin.getSkillGoals().set(shownKey, s.getName(), (Integer) model.getValue(), xp);
|
||
|
|
menu.setVisible(false);
|
||
|
|
refresh();
|
||
|
|
});
|
||
|
|
set.setForeground(Ui.ACCENT);
|
||
|
|
actions.add(set);
|
||
|
|
JButton clear = button("Clear", () ->
|
||
|
|
{
|
||
|
|
plugin.getSkillGoals().clear(shownKey, s.getName());
|
||
|
|
menu.setVisible(false);
|
||
|
|
refresh();
|
||
|
|
});
|
||
|
|
clear.setEnabled(current != null);
|
||
|
|
actions.add(clear);
|
||
|
|
panel.add(actions);
|
||
|
|
|
||
|
|
panel.setPreferredSize(new Dimension(190, panel.getPreferredSize().height));
|
||
|
|
menu.add(panel);
|
||
|
|
menu.show(anchor, Math.max(0, Math.min(x, anchor.getWidth() - 190)), y);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Next round level worth aiming for: 10, 20 .. 80, 85, 90, 99. */
|
||
|
|
private static int nextMilestone(int level)
|
||
|
|
{
|
||
|
|
for (int m : new int[]{10, 20, 30, 40, 50, 60, 70, 80, 85, 90, 99})
|
||
|
|
{
|
||
|
|
if (m > level)
|
||
|
|
{
|
||
|
|
return m;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return level + 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void updateCombo(SkillHistory shown)
|
||
|
|
{
|
||
|
|
List<SkillHistory> all = store.all();
|
||
|
|
List<Choice> choices = new ArrayList<>();
|
||
|
|
for (SkillHistory h : all)
|
||
|
|
{
|
||
|
|
choices.add(new Choice(h.key, h.name == null ? h.key : h.name));
|
||
|
|
}
|
||
|
|
List<Choice> current = new ArrayList<>();
|
||
|
|
for (int i = 0; i < characterBox.getItemCount(); i++)
|
||
|
|
{
|
||
|
|
current.add(characterBox.getItemAt(i));
|
||
|
|
}
|
||
|
|
updatingCombo = true;
|
||
|
|
if (!choices.equals(current))
|
||
|
|
{
|
||
|
|
characterBox.removeAllItems();
|
||
|
|
choices.forEach(characterBox::addItem);
|
||
|
|
}
|
||
|
|
for (Choice c : choices)
|
||
|
|
{
|
||
|
|
if (shown != null && c.key.equals(shown.key))
|
||
|
|
{
|
||
|
|
characterBox.setSelectedItem(c);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
updatingCombo = false;
|
||
|
|
boolean visible = choices.size() > 1;
|
||
|
|
if (characterBox.isVisible() != visible)
|
||
|
|
{
|
||
|
|
characterBox.setVisible(visible);
|
||
|
|
revalidate();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private BufferedImage icon(Skill skill)
|
||
|
|
{
|
||
|
|
return icons.computeIfAbsent(skill, s ->
|
||
|
|
{
|
||
|
|
BufferedImage img = plugin.getSkillIconManager().getSkillImage(s, true);
|
||
|
|
return img == null ? null : ImageUtil.resizeImage(img, 16, 16);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private static String duration(long ms)
|
||
|
|
{
|
||
|
|
if (ms < Ui.MINUTE)
|
||
|
|
{
|
||
|
|
return "-";
|
||
|
|
}
|
||
|
|
long h = ms / Ui.HOUR;
|
||
|
|
long m = (ms % Ui.HOUR) / Ui.MINUTE;
|
||
|
|
return h > 0 ? h + "h " + m + "m" : m + "m";
|
||
|
|
}
|
||
|
|
|
||
|
|
static Color skillColor(Skill skill)
|
||
|
|
{
|
||
|
|
switch (skill.name())
|
||
|
|
{
|
||
|
|
case "ATTACK": return new Color(220, 80, 60);
|
||
|
|
case "DEFENCE": return new Color(120, 150, 230);
|
||
|
|
case "STRENGTH": return new Color(60, 190, 120);
|
||
|
|
case "HITPOINTS": return new Color(230, 90, 90);
|
||
|
|
case "RANGED": return new Color(150, 200, 60);
|
||
|
|
case "PRAYER": return new Color(235, 225, 170);
|
||
|
|
case "MAGIC": return new Color(110, 140, 255);
|
||
|
|
case "COOKING": return new Color(180, 100, 210);
|
||
|
|
case "WOODCUTTING": return new Color(110, 180, 80);
|
||
|
|
case "FLETCHING": return new Color(40, 190, 170);
|
||
|
|
case "FISHING": return new Color(130, 170, 220);
|
||
|
|
case "FIREMAKING": return new Color(250, 150, 40);
|
||
|
|
case "CRAFTING": return new Color(200, 150, 100);
|
||
|
|
case "SMITHING": return new Color(170, 170, 140);
|
||
|
|
case "MINING": return new Color(120, 180, 210);
|
||
|
|
case "HERBLORE": return new Color(60, 200, 70);
|
||
|
|
case "AGILITY": return new Color(110, 115, 220);
|
||
|
|
case "THIEVING": return new Color(190, 110, 170);
|
||
|
|
case "SLAYER": return new Color(200, 70, 70);
|
||
|
|
case "FARMING": return new Color(130, 200, 90);
|
||
|
|
case "RUNECRAFT": return new Color(230, 200, 70);
|
||
|
|
case "HUNTER": return new Color(170, 160, 110);
|
||
|
|
case "CONSTRUCTION": return new Color(200, 180, 140);
|
||
|
|
case "SAILING": return new Color(70, 170, 230);
|
||
|
|
default: return Ui.GOLD;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static JButton button(String text, Runnable action)
|
||
|
|
{
|
||
|
|
JButton b = new JButton(text);
|
||
|
|
b.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
b.setFocusPainted(false);
|
||
|
|
b.setMargin(new java.awt.Insets(2, 2, 2, 2));
|
||
|
|
b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||
|
|
b.addActionListener(e -> action.run());
|
||
|
|
return b;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static final class Choice
|
||
|
|
{
|
||
|
|
final String key;
|
||
|
|
final String name;
|
||
|
|
|
||
|
|
Choice(String key, String name)
|
||
|
|
{
|
||
|
|
this.key = key;
|
||
|
|
this.name = name;
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public boolean equals(Object o)
|
||
|
|
{
|
||
|
|
return o instanceof Choice && ((Choice) o).key.equals(key) && Objects.equals(((Choice) o).name, name);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public int hashCode()
|
||
|
|
{
|
||
|
|
return key.hashCode();
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public String toString()
|
||
|
|
{
|
||
|
|
return name;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** width of the goal cog hit area at the right of a skill row */
|
||
|
|
private static final int GEAR_HIT = 20;
|
||
|
|
|
||
|
|
/** One skill: icon, level (+gained), xp gained, xp/h, progress to the next level and the goal, if set. */
|
||
|
|
private final class SkillRow extends JComponent
|
||
|
|
{
|
||
|
|
private final int skill;
|
||
|
|
private int xp;
|
||
|
|
private int levelsGained;
|
||
|
|
private long gained;
|
||
|
|
private long xph;
|
||
|
|
private long maxGain;
|
||
|
|
private boolean selected;
|
||
|
|
private SkillGoals.Goal goal;
|
||
|
|
private boolean hover;
|
||
|
|
private boolean gearHover;
|
||
|
|
|
||
|
|
SkillRow(int skill)
|
||
|
|
{
|
||
|
|
this.skill = skill;
|
||
|
|
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||
|
|
MouseAdapter mouse = new MouseAdapter()
|
||
|
|
{
|
||
|
|
@Override
|
||
|
|
public void mouseEntered(MouseEvent e)
|
||
|
|
{
|
||
|
|
hover = true;
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void mouseExited(MouseEvent e)
|
||
|
|
{
|
||
|
|
hover = false;
|
||
|
|
gearHover = false;
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void mouseMoved(MouseEvent e)
|
||
|
|
{
|
||
|
|
boolean g = onGear(e.getX(), e.getY());
|
||
|
|
if (g != gearHover)
|
||
|
|
{
|
||
|
|
gearHover = g;
|
||
|
|
setToolTipText(g ? "Set a level goal" : tooltip());
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public void mousePressed(MouseEvent e)
|
||
|
|
{
|
||
|
|
if (SwingUtilities.isRightMouseButton(e) || onGear(e.getX(), e.getY()))
|
||
|
|
{
|
||
|
|
showGoalEditor(skill, SkillRow.this, e.getX(), e.getY());
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
selectSkill(skill);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
addMouseListener(mouse);
|
||
|
|
addMouseMotionListener(mouse);
|
||
|
|
}
|
||
|
|
|
||
|
|
private boolean onGear(int x, int y)
|
||
|
|
{
|
||
|
|
return x >= getWidth() - GEAR_HIT && y <= 20;
|
||
|
|
}
|
||
|
|
|
||
|
|
private String tooltip()
|
||
|
|
{
|
||
|
|
String t = String.format(Locale.US, "<html><b>%s</b><br>%,d xp<br>+%,d xp gained", SkillHistory.SKILLS[skill].getName(), xp, gained);
|
||
|
|
if (goal != null)
|
||
|
|
{
|
||
|
|
t += String.format(Locale.US, "<br>Goal %d: %,d xp left", goal.level, goal.xpLeft(xp));
|
||
|
|
}
|
||
|
|
return t + "<br><i>Right-click or the cog to set a goal</i></html>";
|
||
|
|
}
|
||
|
|
|
||
|
|
void update(int xp, int levelsGained, long gained, long xph, long maxGain, boolean selected, SkillGoals.Goal goal)
|
||
|
|
{
|
||
|
|
boolean resize = (goal == null) != (this.goal == null);
|
||
|
|
this.goal = goal;
|
||
|
|
this.xp = xp;
|
||
|
|
this.levelsGained = levelsGained;
|
||
|
|
this.gained = gained;
|
||
|
|
this.xph = xph;
|
||
|
|
this.maxGain = maxGain;
|
||
|
|
this.selected = selected;
|
||
|
|
setToolTipText(tooltip());
|
||
|
|
if (resize)
|
||
|
|
{
|
||
|
|
revalidate();
|
||
|
|
}
|
||
|
|
repaint();
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public Dimension getPreferredSize()
|
||
|
|
{
|
||
|
|
if (goal == null)
|
||
|
|
{
|
||
|
|
return new Dimension(10, 34);
|
||
|
|
}
|
||
|
|
int asc = getFontMetrics(FontManager.getRunescapeSmallFont()).getAscent();
|
||
|
|
// goal text baseline at 20 + 3 * asc, then gap, 4px bar, bottom padding
|
||
|
|
return new Dimension(10, 4 + 3 * asc + 16 + 14);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
protected void paintComponent(Graphics g0)
|
||
|
|
{
|
||
|
|
Graphics2D g = Ui.prepare(g0);
|
||
|
|
int w = getWidth();
|
||
|
|
int h = getHeight();
|
||
|
|
Skill s = SkillHistory.SKILLS[skill];
|
||
|
|
Color color = skillColor(s);
|
||
|
|
boolean trained = gained > 0;
|
||
|
|
|
||
|
|
g.setColor(selected ? Ui.CARD_SELECTED : hover ? Ui.CARD_HOVER : Ui.CARD);
|
||
|
|
g.fillRoundRect(0, 0, w, h, 6, 6);
|
||
|
|
if (selected)
|
||
|
|
{
|
||
|
|
g.setColor(color);
|
||
|
|
g.fillRoundRect(0, 0, 3, h, 3, 3);
|
||
|
|
}
|
||
|
|
// share of the top skill's gain as a faint background bar
|
||
|
|
if (trained && maxGain > 0)
|
||
|
|
{
|
||
|
|
g.setColor(Ui.alpha(color, 26));
|
||
|
|
g.fillRoundRect(0, 0, (int) (w * (double) gained / maxGain), h, 6, 6);
|
||
|
|
}
|
||
|
|
|
||
|
|
BufferedImage img = icon(s);
|
||
|
|
if (img != null)
|
||
|
|
{
|
||
|
|
g.drawImage(img, 6, (h - 16) / 2 - 2, null);
|
||
|
|
}
|
||
|
|
|
||
|
|
int level = SkillHistory.level(xp);
|
||
|
|
g.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
FontMetrics fm = g.getFontMetrics();
|
||
|
|
int x = 28;
|
||
|
|
int base1 = 4 + fm.getAscent();
|
||
|
|
int base2 = base1 + fm.getAscent() + 2;
|
||
|
|
g.setColor(trained ? Ui.TEXT : Ui.MUTED);
|
||
|
|
g.drawString(s.getName(), x, base1);
|
||
|
|
String lv = "Lv " + level;
|
||
|
|
g.setColor(Ui.MUTED);
|
||
|
|
g.drawString(lv, x, base2);
|
||
|
|
if (levelsGained > 0)
|
||
|
|
{
|
||
|
|
g.setColor(Ui.UP);
|
||
|
|
g.drawString(" +" + levelsGained, x + fm.stringWidth(lv), base2);
|
||
|
|
}
|
||
|
|
|
||
|
|
paintGear(g, w - 11, 10, gearHover ? Ui.ACCENT : goal != null ? Ui.GOLD : hover ? Ui.MUTED : Ui.FAINT);
|
||
|
|
int right = w - GEAR_HIT - 2;
|
||
|
|
g.setColor(trained ? Ui.UP : Ui.FAINT);
|
||
|
|
Ui.drawRight(g, trained ? "+" + Ui.gp(gained) + " xp" : "-", right, base1);
|
||
|
|
if (xph > 0)
|
||
|
|
{
|
||
|
|
g.setColor(Ui.MUTED);
|
||
|
|
Ui.drawRight(g, Ui.gp(xph) + "/h", right, base2);
|
||
|
|
}
|
||
|
|
|
||
|
|
// progress to next level
|
||
|
|
int levelBarY = goal == null ? h - 4 : base2 + 6;
|
||
|
|
if (level < Experience.MAX_VIRT_LEVEL)
|
||
|
|
{
|
||
|
|
int lo = Experience.getXpForLevel(level);
|
||
|
|
int hi = Experience.getXpForLevel(level + 1);
|
||
|
|
double p = hi > lo ? (double) (xp - lo) / (hi - lo) : 0;
|
||
|
|
int bx = x;
|
||
|
|
int bw = w - x - 7;
|
||
|
|
g.setColor(Ui.alpha(Color.WHITE, 18));
|
||
|
|
g.fillRect(bx, levelBarY, bw, 2);
|
||
|
|
g.setColor(Ui.alpha(color, trained ? 220 : 110));
|
||
|
|
g.fillRect(bx, levelBarY, (int) (bw * Math.max(0, Math.min(1, p))), 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (goal != null)
|
||
|
|
{
|
||
|
|
int left = goal.xpLeft(xp);
|
||
|
|
boolean reached = left == 0;
|
||
|
|
int base3 = levelBarY + 2 + 6 + fm.getAscent();
|
||
|
|
String head = "Goal " + goal.level + " ";
|
||
|
|
g.setColor(Ui.MUTED);
|
||
|
|
g.drawString(head, x, base3);
|
||
|
|
g.setColor(reached ? Ui.UP : Ui.GOLD);
|
||
|
|
g.drawString(percent(goal.progress(xp)), x + fm.stringWidth(head), base3);
|
||
|
|
String info = reached ? "reached!" : Ui.gp(left) + " xp left" + (xph > 0 ? " ~" + duration(left * Ui.HOUR / xph) : "");
|
||
|
|
g.setColor(reached ? Ui.UP : Ui.TEXT);
|
||
|
|
Ui.drawRight(g, info, w - 7, base3);
|
||
|
|
int bx = x;
|
||
|
|
int bw = w - x - 7;
|
||
|
|
int by = base3 + 5;
|
||
|
|
g.setColor(Ui.alpha(Color.WHITE, 18));
|
||
|
|
g.fillRoundRect(bx, by, bw, 4, 4, 4);
|
||
|
|
g.setColor(reached ? Ui.UP : Ui.GOLD);
|
||
|
|
g.fillRoundRect(bx, by, Math.max(4, (int) (bw * goal.progress(xp))), 4, 4, 4);
|
||
|
|
}
|
||
|
|
g.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Small cog: ring with eight teeth. */
|
||
|
|
private void paintGear(Graphics2D g, int cx, int cy, Color c)
|
||
|
|
{
|
||
|
|
Graphics2D g2 = (Graphics2D) g.create();
|
||
|
|
g2.translate(cx, cy);
|
||
|
|
g2.setColor(c);
|
||
|
|
for (int i = 0; i < 8; i++)
|
||
|
|
{
|
||
|
|
g2.fillRect(-1, -6, 3, 3);
|
||
|
|
g2.rotate(Math.PI / 4);
|
||
|
|
}
|
||
|
|
g2.fillOval(-4, -4, 9, 9);
|
||
|
|
g2.setColor(selected ? Ui.CARD_SELECTED : hover ? Ui.CARD_HOVER : Ui.CARD);
|
||
|
|
g2.fillOval(-1, -1, 3, 3);
|
||
|
|
g2.dispose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** "Attack 70 → 71" with the time it happened. */
|
||
|
|
private final class LevelUpRow extends JComponent
|
||
|
|
{
|
||
|
|
private final SkillHistory.LevelUp levelUp;
|
||
|
|
|
||
|
|
LevelUpRow(SkillHistory.LevelUp levelUp)
|
||
|
|
{
|
||
|
|
this.levelUp = levelUp;
|
||
|
|
setToolTipText(Instant.ofEpochMilli(levelUp.t).atZone(ZoneId.systemDefault()).format(WHEN));
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
public Dimension getPreferredSize()
|
||
|
|
{
|
||
|
|
return new Dimension(10, 22);
|
||
|
|
}
|
||
|
|
|
||
|
|
@Override
|
||
|
|
protected void paintComponent(Graphics g0)
|
||
|
|
{
|
||
|
|
Graphics2D g = Ui.prepare(g0);
|
||
|
|
int w = getWidth();
|
||
|
|
int h = getHeight();
|
||
|
|
Skill s = SkillHistory.SKILLS[levelUp.skill];
|
||
|
|
g.setColor(Ui.CARD);
|
||
|
|
g.fillRoundRect(0, 0, w, h, 6, 6);
|
||
|
|
BufferedImage img = icon(s);
|
||
|
|
if (img != null)
|
||
|
|
{
|
||
|
|
g.drawImage(img, 5, (h - 16) / 2, null);
|
||
|
|
}
|
||
|
|
g.setFont(FontManager.getRunescapeSmallFont());
|
||
|
|
FontMetrics fm = g.getFontMetrics();
|
||
|
|
int base = (h + fm.getAscent()) / 2 - 1;
|
||
|
|
g.setColor(Ui.TEXT);
|
||
|
|
String name = s.getName() + " ";
|
||
|
|
g.drawString(name, 26, base);
|
||
|
|
g.setColor(skillColor(s));
|
||
|
|
String lv = levelUp.to - levelUp.from > 1 ? levelUp.from + " > " + levelUp.to : String.valueOf(levelUp.to);
|
||
|
|
g.drawString(lv, 26 + fm.stringWidth(name), base);
|
||
|
|
g.setColor(Ui.MUTED);
|
||
|
|
String when = Ui.ago(levelUp.t);
|
||
|
|
if (System.currentTimeMillis() - levelUp.t >= Ui.DAY)
|
||
|
|
{
|
||
|
|
when = Instant.ofEpochMilli(levelUp.t).atZone(ZoneId.systemDefault()).format(WHEN);
|
||
|
|
}
|
||
|
|
Ui.drawRight(g, when, w - 7, base);
|
||
|
|
g.dispose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private static final String[][] HUD_OPTIONS = {
|
||
|
|
{"skillsHudLevel", "Level"},
|
||
|
|
{"skillsHudPercent", "Progress %"},
|
||
|
|
{"skillsHudXpLeft", "Xp to level"},
|
||
|
|
{"skillsHudTimeToLevel", "Time to level"},
|
||
|
|
{"skillsHudXpHour", "Xp per hour"},
|
||
|
|
{"skillsHudProgressBar", "Progress bar"},
|
||
|
|
};
|
||
|
|
|
||
|
|
private void showHudOptions(JComponent anchor)
|
||
|
|
{
|
||
|
|
JPopupMenu menu = new JPopupMenu();
|
||
|
|
for (String[] option : HUD_OPTIONS)
|
||
|
|
{
|
||
|
|
JCheckBoxMenuItem item = new JCheckBoxMenuItem(option[1], plugin.isHudOption(option[0]));
|
||
|
|
item.addActionListener(e -> plugin.setHudOption(option[0], item.isSelected()));
|
||
|
|
menu.add(item);
|
||
|
|
}
|
||
|
|
menu.show(anchor, 0, anchor.getHeight());
|
||
|
|
}
|
||
|
|
}
|