From 12453c60da8b6b0f80cda312d4e19e2a6682104f Mon Sep 17 00:00:00 2001 From: XxFran10xX Date: Wed, 30 Sep 2026 18:42:26 +0200 Subject: [PATCH] Fix weekly reward previews with compact random ranges --- .../activitytf/gui/ActivityGui.java | 33 ++++-- .../activitytf/gui/RewardPreview.java | 62 +++++++++++ src/main/resources/messages.yml | 4 + .../activitytf/gui/ActivityGuiTest.java | 103 ++++++++++++++++-- 4 files changed, 180 insertions(+), 22 deletions(-) create mode 100644 src/main/java/net/tfminecraft/activitytf/gui/RewardPreview.java diff --git a/src/main/java/net/tfminecraft/activitytf/gui/ActivityGui.java b/src/main/java/net/tfminecraft/activitytf/gui/ActivityGui.java index 7608262..d0c026a 100644 --- a/src/main/java/net/tfminecraft/activitytf/gui/ActivityGui.java +++ b/src/main/java/net/tfminecraft/activitytf/gui/ActivityGui.java @@ -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 { @@ -124,7 +125,7 @@ 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()), @@ -132,7 +133,7 @@ private ItemStack barItem(ActivityConfiguration config, Messages messages, Playe } static List weeklyRewardLore(Messages messages, PlayerData data, List milestones, - Map drops, int multiplier, + Map drops, int multiplier, Set multiplierPools, Function> pools) { List lore = new ArrayList<>(); int due = data.claimable(milestones); @@ -142,14 +143,28 @@ static List weeklyRewardLore(Messages messages, PlayerData data, List 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) { @@ -171,13 +186,7 @@ static List nextRewardDisplays(int milestone, Map if (entries == null || entries.isEmpty()) { return List.of(); } - List 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 milestones) { diff --git a/src/main/java/net/tfminecraft/activitytf/gui/RewardPreview.java b/src/main/java/net/tfminecraft/activitytf/gui/RewardPreview.java new file mode 100644 index 0000000..f3b1258 --- /dev/null +++ b/src/main/java/net/tfminecraft/activitytf/gui/RewardPreview.java @@ -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 summarize(List entries) { + Map 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 displays = new ArrayList<>(); + options.forEach((key, range) -> displays.add(key instanceof Key ? range.display() : range.label())); + return displays; + } +} diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 94866f1..7031f73 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -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%" diff --git a/src/test/java/net/tfminecraft/activitytf/gui/ActivityGuiTest.java b/src/test/java/net/tfminecraft/activitytf/gui/ActivityGuiTest.java index 9ea6bae..f07e240 100644 --- a/src/test/java/net/tfminecraft/activitytf/gui/ActivityGuiTest.java +++ b/src/test/java/net/tfminecraft/activitytf/gui/ActivityGuiTest.java @@ -269,7 +269,7 @@ void theWeeklyIconNamesTheNextReward() { List.of(new RewardEntry.Item("m.material.steel", 3))); List 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); @@ -277,20 +277,20 @@ void theWeeklyIconNamesTheNextReward() { } @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 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 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 @@ -299,7 +299,7 @@ void theWeeklyIconSaysThereAreNoMoreRewardsThisWeek() { PlayerData data = new PlayerData(40, 0, "2026-W38", "2026-09-17", 40, java.util.Map.of()); List 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"), @@ -314,12 +314,95 @@ void aReadyRewardStillSaysWhatItIsAndThatItCanBeClaimed() { List.of(new RewardEntry.Item("DIAMOND", 1))); List 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 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 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 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 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 pool = new ArrayList<>(); + for (int i = 0; i < 12; i++) { + pool.add(material(1, 1, "item_" + i, "&7Item " + i)); + } + List 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 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 lore = ActivityGui.activityLore(shippedMessages(), described(List.of("#e6ca40&lX")), 0);