Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions src/main/java/net/tfminecraft/activitytf/gui/ActivityGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

public class ActivityGui implements Listener {
Expand Down Expand Up @@ -124,15 +125,15 @@ private ItemStack barItem(ActivityConfiguration config, Messages messages, Playe
lore.add(Utils.colorize(bar));
lore.add(" ");
lore.addAll(weeklyRewardLore(messages, data, milestones, config.milestoneDrops(),
config.rewardMultiplier(), config::rewardPool));
config.rewardMultiplier(), config.multiplierPools(), config::rewardPool));

return item(Material.EXPERIENCE_BOTTLE,
messages.get("gui.bar-name", "%points%", data.points(), "%max%", config.barMax()),
lore);
}

static List<String> weeklyRewardLore(Messages messages, PlayerData data, List<Integer> milestones,
Map<Integer, RewardEntry> drops, int multiplier,
Map<Integer, RewardEntry> drops, int multiplier, Set<String> multiplierPools,
Function<String, List<RewardEntry>> pools) {
List<String> lore = new ArrayList<>();
int due = data.claimable(milestones);
Expand All @@ -142,14 +143,28 @@ static List<String> weeklyRewardLore(Messages messages, PlayerData data, List<In
return lore;
}

RewardEntry drop = drops.get(next);
String pool = drop == null ? ActivityConfiguration.DEFAULT_POOL
: ActivityConfiguration.referencedPool(drop);
List<String> rewards = nextRewardDisplays(next, drops, multiplier, pools);
if (rewards.size() == 1) {
if (pool == null && rewards.size() == 1) {
lore.add(messages.get("gui.bar-lore-reward", "%points%", next, "%reward%",
Utils.colorize(rewards.get(0))));
} else {
lore.add(messages.get("gui.bar-lore-next-options", "%points%", next));
for (String reward : rewards) {
lore.add(Utils.colorize(reward));
if (rewards.isEmpty()) {
lore.add(messages.get("gui.reward-preview-unavailable"));
} else {
int draws = multiplierPools.contains(pool) ? multiplier : 1;
lore.add(messages.get(draws == 1 ? "gui.reward-preview-one" : "gui.reward-preview-many",
"%count%", draws));
int shown = Math.min(4, rewards.size());
for (String reward : rewards.subList(0, shown)) {
lore.add(Utils.colorize(reward));
}
if (shown < rewards.size()) {
lore.add(messages.get("gui.reward-preview-more", "%count%", rewards.size() - shown));
}
}
}
if (due > 0) {
Expand All @@ -171,13 +186,7 @@ static List<String> nextRewardDisplays(int milestone, Map<Integer, RewardEntry>
if (entries == null || entries.isEmpty()) {
return List.of();
}
List<String> displays = new ArrayList<>();
for (RewardEntry entry : entries) {
if (entry.display() != null && !entry.display().isBlank()) {
displays.add(entry.display());
}
}
return displays;
return RewardPreview.summarize(entries);
}

private static Integer nextMilestone(PlayerData data, List<Integer> milestones) {
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/net/tfminecraft/activitytf/gui/RewardPreview.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package net.tfminecraft.activitytf.gui;

import net.tfminecraft.activitytf.models.RewardEntry;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

final class RewardPreview {

// Display text supplies the name/colour only. Bounds come from the actual item amounts.
private static final String COLOUR = "(?:#[a-fA-F0-9]{6}|[&§][0-9a-fk-orA-FK-OR])";
private static final Pattern QUANTITY = Pattern.compile("^(?:" + COLOUR + ")*[xX×]\\d+\\s+");

private RewardPreview() {
}

private record Key(String path, String label) {
}

private record Range(String label, int min, int max) {
Range include(int amount) {
return new Range(label, Math.min(min, amount), Math.max(max, amount));
}

String display() {
return "#50d990x" + min + (min == max ? "" : "–" + max) + " &r" + label;
}
}

static List<String> summarize(List<RewardEntry> entries) {
Map<Object, Range> options = new LinkedHashMap<>();
for (RewardEntry entry : entries) {
if (entry.weight() <= 0) {
continue;
}
if (entry.items().size() == 1 && entry.commands().isEmpty()) {
RewardEntry.Item item = entry.items().get(0);
String label = entry.display() == null ? "" : QUANTITY.matcher(entry.display()).replaceFirst("").strip();
if (label.isBlank()) {
label = "&7" + item.path();
}
Key key = new Key(item.path(), label);
Range range = options.get(key);
options.put(key, range == null ? new Range(label, item.amount(), item.amount())
: range.include(item.amount()));
} else {
// Commands and bundles are whole outcomes; do not invent per-item ranges for them.
String label = entry.display();
if (label == null || label.isBlank()) {
label = "&7Other reward";
}
options.putIfAbsent(label, new Range(label, 0, 0));
}
}
List<String> displays = new ArrayList<>();
options.forEach((key, range) -> displays.add(key instanceof Key ? range.display() : range.label()));
return displays;
}
}
4 changes: 4 additions & 0 deletions src/main/resources/messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ gui:
bar-lore-reward: "#d97b66Next reward&e: %reward% #b8906eat &6%points% #b8906epoints"
bar-lore-next-options: "#d97b66Next reward at&e: &6%points% #b8906epoints"
bar-lore-none: "&7No more rewards this week"
reward-preview-one: "&7One random reward from:"
reward-preview-many: "&7%count% random rewards, each from:"
reward-preview-more: "&7+%count% other possible rewards"
reward-preview-unavailable: "&8Reward preview unavailable"
reward-click: "#6b9c68[#fafa16Click to claim %count% reward(s)#6b9c68]"
activity-lore-progress: "#af97bdProgress&e: %bar%"
activity-lore-today: "#d97b66Today&e: &6%today%"
Expand Down
103 changes: 93 additions & 10 deletions src/test/java/net/tfminecraft/activitytf/gui/ActivityGuiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -269,28 +269,28 @@ void theWeeklyIconNamesTheNextReward() {
List.of(new RewardEntry.Item("m.material.steel", 3)));

List<String> lore = ActivityGui.weeklyRewardLore(messages, data, List.of(10, 20),
Map.of(10, steel), 1, pool -> List.of());
Map.of(10, steel), 1, Set.of(), pool -> List.of());

assertEquals(List.of(messages.get("gui.bar-lore-reward", "%points%", 10, "%reward%",
Utils.colorize("#50d990x3 #b8906eSteel"))), lore);
assertTrue(String.join(" ", lore).contains("Steel"), lore.toString());
}

@Test
void theWeeklyIconListsEachDrawInTheNextPool() {
void theWeeklyIconSummarizesTheNextPoolAsOneRandomRange() {
Messages messages = shippedMessages();
PlayerData data = new PlayerData(12, 0, "2026-W38", "2026-09-17", 10, java.util.Map.of());
List<RewardEntry> pool = List.of(
new RewardEntry(65, "#50d990x2 #7f7d80Ignitium", List.of(), List.of()),
new RewardEntry(3, "#50d990x8 #7f7d80Ignitium", List.of(), List.of()));
material(65, 2, "ignitium", "#7f7d80Ignitium"),
material(3, 8, "ignitium", "#7f7d80Ignitium"));

List<String> lore = ActivityGui.weeklyRewardLore(messages, data, List.of(10, 20),
Map.of(20, ActivityConfiguration.poolRef("pool_prologue")), 1, name -> pool);
Map.of(20, ActivityConfiguration.poolRef("pool_prologue")), 1, Set.of(), name -> pool);

assertEquals(messages.get("gui.bar-lore-next-options", "%points%", 20), lore.get(0));
assertTrue(lore.get(1).contains("x2"), lore.toString());
assertTrue(lore.get(2).contains("x8"), lore.toString());
assertFalse(lore.get(1).contains("#50d990"), lore.toString());
assertEquals(messages.get("gui.reward-preview-one"), lore.get(1));
assertEquals(Utils.colorize("#50d990x2–8 &r#7f7d80Ignitium"), lore.get(2));
assertEquals(3, lore.size());
}

@Test
Expand All @@ -299,7 +299,7 @@ void theWeeklyIconSaysThereAreNoMoreRewardsThisWeek() {
PlayerData data = new PlayerData(40, 0, "2026-W38", "2026-09-17", 40, java.util.Map.of());

List<String> lore = ActivityGui.weeklyRewardLore(messages, data, List.of(10, 20, 40),
Map.of(), 1, pool -> List.of());
Map.of(), 1, Set.of(), pool -> List.of());

assertEquals(List.of(messages.get("gui.bar-lore-none")), lore);
assertTrue(lore.get(0).toLowerCase(java.util.Locale.ROOT).contains("no more rewards this week"),
Expand All @@ -314,12 +314,95 @@ void aReadyRewardStillSaysWhatItIsAndThatItCanBeClaimed() {
List.of(new RewardEntry.Item("DIAMOND", 1)));

List<String> lore = ActivityGui.weeklyRewardLore(messages, data, List.of(10),
Map.of(10, steel), 2, pool -> List.of());
Map.of(10, steel), 2, Set.of(), pool -> List.of());

assertTrue(lore.get(0).contains("x2"), lore.toString());
assertEquals(messages.get("gui.reward-click", "%count%", 1), lore.get(1));
}

private static RewardEntry material(int weight, int amount, String id, String label) {
return new RewardEntry(weight, "#50d990x" + amount + " " + label, List.of(),
List.of(new RewardEntry.Item("m.materials." + id, amount)));
}

@Test
void actOneShowsThreeMaterialRangesPerIndependentDraw() {
Messages messages = shippedMessages();
List<RewardEntry> pool = new ArrayList<>();
int[][] weights = {{35, 15, 15}, {13, 6, 6}, {3, 2, 2}, {1, 1, 1}};
for (int tier = 0; tier < 4; tier++) {
pool.add(material(weights[tier][0], (tier + 1) * 20, "ignitium", "#7f7d80Ignitium"));
pool.add(material(weights[tier][1], (tier + 1) * 2, "raw_tin", "#7f7d80Raw Tin"));
pool.add(material(weights[tier][2], (tier + 1) * 2, "abyssalite_fragment", "#3b4e60Abyssalite Fragment"));
}
PlayerData data = new PlayerData(10, 0, "2026-W38", "2026-09-17", 0, Map.of());
List<String> lore = ActivityGui.weeklyRewardLore(messages, data, List.of(10),
Map.of(10, ActivityConfiguration.poolRef("pool_act1")), 2, Set.of("pool_act1"), name -> pool);

assertEquals(messages.get("gui.reward-preview-many", "%count%", 2), lore.get(1));
assertEquals(Utils.colorize("#50d990x20–80 &r#7f7d80Ignitium"), lore.get(2));
assertEquals(Utils.colorize("#50d990x2–8 &r#7f7d80Raw Tin"), lore.get(3));
assertEquals(Utils.colorize("#50d990x2–8 &r#3b4e60Abyssalite Fragment"), lore.get(4));
assertEquals(messages.get("gui.reward-click", "%count%", 1), lore.get(5));
}

@Test
void unlistedSkinPoolStillPreviewsOnlyOneDraw() {
Messages messages = shippedMessages();
PlayerData data = new PlayerData(0, 0, "2026-W38", "2026-09-17", 0, Map.of());
List<String> lore = ActivityGui.weeklyRewardLore(messages, data, List.of(10),
Map.of(10, ActivityConfiguration.poolRef("pool_skin")), 2, Set.of("pool_act1"),
name -> List.of(material(65, 1, "common_item_skin_scroll", "&7Common Item Skin Scroll"),
material(3, 1, "legendary_item_skin_scroll", "&7Legendary Item Skin Scroll")));

assertEquals(messages.get("gui.reward-preview-one"), lore.get(1));
assertTrue(lore.get(2).contains("x1"));
assertTrue(lore.get(3).contains("x1"));
}

@Test
void previewBoundsUseItemsAndNeverMergeDifferentItemsWithTheSameName() {
List<String> options = RewardPreview.summarize(List.of(
new RewardEntry(65, "#50d990x999 &7Metal", List.of(),
List.of(new RewardEntry.Item("m.materials.ignitium", 2))),
material(3, 8, "ignitium", "&7Metal"),
material(25, 4, "raw_tin", "&7Metal"),
material(0, 1000, "ignitium", "&7Metal")));
assertEquals(List.of("#50d990x2–8 &r&7Metal", "#50d990x4 &r&7Metal"), options);
}

@Test
void commandAndBundleOutcomesKeepTheirDescriptions() {
RewardEntry command = new RewardEntry(1, "&7A title", List.of("title %player%"), List.of());
RewardEntry bundle = new RewardEntry(1, "&7Material bundle", List.of(),
List.of(new RewardEntry.Item("DIAMOND", 2), new RewardEntry.Item("IRON_INGOT", 3)));
assertEquals(List.of("&7A title", "&7Material bundle"),
RewardPreview.summarize(List.of(command, bundle, command)));
}

@Test
void largePoolsAreBoundedAndMentionTheRemainingPossibilities() {
Messages messages = shippedMessages();
PlayerData data = new PlayerData(0, 0, "2026-W38", "2026-09-17", 0, Map.of());
List<RewardEntry> pool = new ArrayList<>();
for (int i = 0; i < 12; i++) {
pool.add(material(1, 1, "item_" + i, "&7Item " + i));
}
List<String> lore = ActivityGui.weeklyRewardLore(messages, data, List.of(10), Map.of(),
1, Set.of(), name -> pool);
assertEquals(7, lore.size());
assertEquals(messages.get("gui.reward-preview-more", "%count%", 8), lore.get(6));
}

@Test
void anEmptyPoolReportsAnUnavailablePreview() {
Messages messages = shippedMessages();
PlayerData data = new PlayerData(0, 0, "2026-W38", "2026-09-17", 0, Map.of());
List<String> lore = ActivityGui.weeklyRewardLore(messages, data, List.of(10), Map.of(),
1, Set.of(), name -> List.of());
assertEquals(messages.get("gui.reward-preview-unavailable"), lore.get(1));
}

@Test
void descriptionLinesAreColorized() {
List<String> lore = ActivityGui.activityLore(shippedMessages(), described(List.of("#e6ca40&lX")), 0);
Expand Down