Skip to content

Commit fd8cab4

Browse files
authored
Merge pull request #419 from BentoBoxWorld/feat/335-biome-requirement
Add biome requirement to island challenges (#335)
2 parents bb27ccc + 2f74680 commit fd8cab4

10 files changed

Lines changed: 403 additions & 5 deletions

File tree

src/main/java/world/bentobox/challenges/database/object/requirements/IslandRequirements.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.HashSet;
1313
import java.util.Map;
1414
import java.util.Objects;
15+
import java.util.Set;
1516

1617
import org.bukkit.Fluid;
1718
import org.bukkit.Material;
@@ -79,6 +80,15 @@ public IslandRequirements() {
7980
@Expose
8081
private int searchRadius = 10;
8182

83+
/**
84+
* Namespaced keys (e.g. "minecraft:plains") of biomes the player must be standing in to
85+
* complete the challenge. Stored as key strings rather than Biome objects so serialization
86+
* is version-robust (Biome is a registry-backed type) and unknown biomes simply never match.
87+
* Empty means no biome restriction.
88+
*/
89+
@Expose
90+
private Set<String> requiredBiomes = new HashSet<>();
91+
8292
// ---------------------------------------------------------------------
8393
// Section: Getters and Setters
8494
// ---------------------------------------------------------------------
@@ -182,6 +192,25 @@ public void setSearchRadius(int searchRadius) {
182192
this.searchRadius = searchRadius;
183193
}
184194

195+
196+
/**
197+
* @return the namespaced keys of biomes the player must stand in (empty for no restriction).
198+
*/
199+
public Set<String> getRequiredBiomes() {
200+
if (this.requiredBiomes == null) {
201+
this.requiredBiomes = new HashSet<>();
202+
}
203+
return requiredBiomes;
204+
}
205+
206+
207+
/**
208+
* @param requiredBiomes the required biome keys to set.
209+
*/
210+
public void setRequiredBiomes(Set<String> requiredBiomes) {
211+
this.requiredBiomes = requiredBiomes;
212+
}
213+
185214
/**
186215
* Method isValid returns if given requirement data is valid or not.
187216
*
@@ -215,6 +244,7 @@ public Requirements copy() {
215244
clone.setRemoveEntities(this.removeEntities);
216245

217246
clone.setSearchRadius(this.searchRadius);
247+
clone.setRequiredBiomes(new HashSet<>(this.getRequiredBiomes()));
218248

219249
return clone;
220250
}

src/main/java/world/bentobox/challenges/panel/CommonPanel.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,17 @@ private String generateIslandChallenge(IslandRequirements requirement) {
398398
entities.insert(0, this.user.getTranslationOrNothing(reference + "entities-title"));
399399
}
400400

401+
// Required biomes
402+
StringBuilder biomes = new StringBuilder();
403+
if (!requirement.getRequiredBiomes().isEmpty()) {
404+
biomes.append(this.user.getTranslationOrNothing(reference + "biomes-title"));
405+
requirement.getRequiredBiomes().stream().sorted().forEach(biomeKey -> {
406+
biomes.append("\n");
407+
biomes.append(this.user.getTranslationOrNothing(reference + "biome-value", "[biome]",
408+
Utils.prettifyBiome(biomeKey)));
409+
});
410+
}
411+
401412
String searchRadius = this.user.getTranslationOrNothing(reference + "search-radius", Constants.PARAMETER_NUMBER,
402413
String.valueOf(requirement.getSearchRadius()));
403414

@@ -409,7 +420,7 @@ private String generateIslandChallenge(IslandRequirements requirement) {
409420
: "";
410421

411422
return this.user.getTranslationOrNothing(reference + "lore", "[blocks]", blocks.toString(), "[entities]",
412-
entities.toString(),
423+
entities.toString(), "[biomes]", biomes.toString(),
413424
"[warning-block]", warningBlocks, "[warning-entity]", warningEntities, "[search-radius]", searchRadius);
414425
}
415426

src/main/java/world/bentobox/challenges/panel/admin/EditChallengePanel.java

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
import java.util.Comparator;
88
import java.util.HashSet;
99
import java.util.List;
10+
import java.util.Objects;
1011
import java.util.Set;
1112
import java.util.function.Consumer;
1213
import java.util.stream.Collectors;
1314

1415
import org.bukkit.Material;
16+
import org.bukkit.NamespacedKey;
17+
import org.bukkit.Registry;
1518
import org.bukkit.World;
19+
import org.bukkit.block.Biome;
1620
import org.bukkit.event.inventory.InventoryClickEvent;
1721
import org.bukkit.event.inventory.InventoryCloseEvent;
1822
import org.bukkit.inventory.ItemStack;
@@ -35,6 +39,7 @@
3539
import world.bentobox.challenges.panel.ConversationUtils;
3640
import world.bentobox.challenges.panel.util.EnvironmentSelector;
3741
import world.bentobox.challenges.panel.util.ItemSelector;
42+
import world.bentobox.challenges.panel.util.MultiBiomeSelector;
3843
import world.bentobox.challenges.panel.util.MultiBlockSelector;
3944
import world.bentobox.challenges.utils.Constants;
4045
import world.bentobox.challenges.utils.Utils;
@@ -190,6 +195,7 @@ private void buildIslandRequirementsPanel(PanelBuilder panelBuilder) {
190195

191196

192197
panelBuilder.item(23, this.createRequirementButton(RequirementButton.SEARCH_RADIUS));
198+
panelBuilder.item(24, this.createRequirementButton(RequirementButton.REQUIRED_BIOMES));
193199
panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS));
194200
}
195201

@@ -740,6 +746,11 @@ private PanelItem createRequirementButton(RequirementButton button) {
740746
REQUIRED_MATERIALTAGS, REQUIRED_ENTITYTAGS -> {
741747
return this.createIslandRequirementButton(button);
742748
}
749+
// Biome requirement is a self-contained island requirement, kept out of the large
750+
// createIslandRequirementButton switch.
751+
case REQUIRED_BIOMES -> {
752+
return this.createBiomeRequirementButton();
753+
}
743754
// Buttons for Inventory Requirements
744755
case REQUIRED_ITEMS, REMOVE_ITEMS, ADD_IGNORED_META, REMOVE_IGNORED_META -> {
745756
return this.createInventoryRequirementButton(button);
@@ -933,6 +944,68 @@ private PanelItem createIslandRequirementButton(RequirementButton button) {
933944
.clickHandler(clickHandler).build();
934945
}
935946

947+
948+
/**
949+
* Creates the "Required Biomes" button for island challenges. Left-click opens the biome
950+
* selector (hiding already-chosen biomes); right-click clears the list.
951+
*
952+
* @return the PanelItem for the biome requirement button.
953+
*/
954+
private PanelItem createBiomeRequirementButton() {
955+
final String reference = Constants.BUTTON + RequirementButton.REQUIRED_BIOMES.name().toLowerCase() + ".";
956+
final String name = this.user.getTranslation(reference + "name");
957+
final IslandRequirements requirements = this.challenge.getRequirements();
958+
final List<String> description = new ArrayList<>();
959+
description.add(this.user.getTranslation(reference + Constants.DESCRIPTION_KEY));
960+
961+
if (requirements.getRequiredBiomes().isEmpty()) {
962+
description.add(this.user.getTranslation(reference + "none"));
963+
} else {
964+
description.add(this.user.getTranslation(reference + Constants.TITLE_KEY));
965+
requirements.getRequiredBiomes().forEach(biomeKey -> description.add(
966+
this.user.getTranslation(reference + "list", "[biome]", Utils.prettifyBiome(biomeKey))));
967+
}
968+
969+
description.add("");
970+
description.add(this.user.getTranslation(Constants.CLICK_TO_ADD));
971+
972+
if (!requirements.getRequiredBiomes().isEmpty()) {
973+
description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-clear"));
974+
}
975+
976+
return new PanelItemBuilder().icon(new ItemStack(Material.GRASS_BLOCK)).name(name).description(description)
977+
.glow(false).clickHandler((panel, user, clickType, slot) -> {
978+
if (clickType.isRightClick()) {
979+
requirements.getRequiredBiomes().clear();
980+
this.build();
981+
} else {
982+
this.openBiomeSelector(requirements);
983+
}
984+
return true;
985+
}).build();
986+
}
987+
988+
989+
/**
990+
* Opens the biome selector for the given island requirements, excluding already-chosen
991+
* biomes, and adds the chosen biomes back to the requirement before rebuilding the panel.
992+
*
993+
* @param requirements the island requirements being edited.
994+
*/
995+
private void openBiomeSelector(IslandRequirements requirements) {
996+
Set<Biome> excluded = requirements.getRequiredBiomes().stream()
997+
.map(key -> Registry.BIOME.get(NamespacedKey.fromString(key)))
998+
.filter(Objects::nonNull).collect(Collectors.toSet());
999+
1000+
MultiBiomeSelector.open(this.user, excluded, (status, biomes) -> {
1001+
if (Boolean.TRUE.equals(status) && biomes != null) {
1002+
biomes.forEach(biome -> requirements.getRequiredBiomes().add(MultiBiomeSelector.biomeKey(biome)));
1003+
}
1004+
1005+
this.build();
1006+
});
1007+
}
1008+
9361009
/**
9371010
* This method creates buttons for inventory requirements menu.
9381011
*
@@ -1090,7 +1163,7 @@ private PanelItem createInventoryRequirementButton(RequirementButton button) {
10901163
glow = false;
10911164

10921165
description.add("");
1093-
description.add(this.user.getTranslation(Constants.TIPS + "click-to-add"));
1166+
description.add(this.user.getTranslation(Constants.CLICK_TO_ADD));
10941167
}
10951168
case REMOVE_IGNORED_META -> {
10961169
icon = new ItemStack(Material.RED_SHULKER_BOX);
@@ -1795,7 +1868,7 @@ private PanelItem createRewardButton(RewardButton button) {
17951868
glow = false;
17961869

17971870
description.add("");
1798-
description.add(this.user.getTranslation(Constants.TIPS + "click-to-add"));
1871+
description.add(this.user.getTranslation(Constants.CLICK_TO_ADD));
17991872
}
18001873
case REMOVE_IGNORED_META -> {
18011874
icon = new ItemStack(Material.RED_SHULKER_BOX);
@@ -1926,7 +1999,7 @@ public enum RequirementButton {
19261999
REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY, STATISTIC, STATISTIC_BLOCKS, STATISTIC_ITEMS,
19272000
STATISTIC_ENTITIES,
19282001
STATISTIC_AMOUNT, REMOVE_STATISTIC, REQUIRED_MATERIALTAGS, REQUIRED_ENTITYTAGS, REQUIRED_STATISTICS,
1929-
REMOVE_STATISTICS, REQUIRED_PAPI, REQUIRED_ADVANCEMENTS,
2002+
REMOVE_STATISTICS, REQUIRED_PAPI, REQUIRED_ADVANCEMENTS, REQUIRED_BIOMES,
19302003
}
19312004

19322005
// ---------------------------------------------------------------------
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package world.bentobox.challenges.panel.util;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.Comparator;
6+
import java.util.HashSet;
7+
import java.util.List;
8+
import java.util.Set;
9+
import java.util.function.BiConsumer;
10+
import java.util.stream.Collectors;
11+
import java.util.stream.StreamSupport;
12+
13+
import org.bukkit.Material;
14+
import org.bukkit.Registry;
15+
import org.bukkit.block.Biome;
16+
import org.bukkit.inventory.ItemStack;
17+
18+
import world.bentobox.bentobox.api.user.User;
19+
import world.bentobox.challenges.utils.Utils;
20+
21+
/**
22+
* A multi-selector GUI for choosing biomes. Extends the unified multi-selector base and
23+
* supplies the biome-specific details (element list, icons, display names).
24+
*
25+
* <p>Biomes are a registry-backed type in modern Minecraft, so they are handled by their
26+
* namespaced key rather than as an enum.
27+
*/
28+
public class MultiBiomeSelector extends UnifiedMultiSelector<Biome> {
29+
30+
private final Set<Biome> excluded;
31+
32+
private MultiBiomeSelector(User user, Set<Biome> excluded,
33+
BiConsumer<Boolean, Collection<Biome>> consumer) {
34+
super(user, Mode.ANY, consumer);
35+
this.excluded = excluded;
36+
}
37+
38+
/**
39+
* Opens the biome selector.
40+
*
41+
* @param user the user who opens the GUI.
42+
* @param excluded biomes to hide from the list (e.g. ones already selected).
43+
* @param consumer callback receiving the confirmation flag and the chosen biomes.
44+
*/
45+
public static void open(User user, Set<Biome> excluded,
46+
BiConsumer<Boolean, Collection<Biome>> consumer) {
47+
new MultiBiomeSelector(user, excluded, consumer).build();
48+
}
49+
50+
/**
51+
* Opens the biome selector with no exclusions.
52+
*
53+
* @param user the user who opens the GUI.
54+
* @param consumer callback receiving the confirmation flag and the chosen biomes.
55+
*/
56+
public static void open(User user, BiConsumer<Boolean, Collection<Biome>> consumer) {
57+
new MultiBiomeSelector(user, new HashSet<>(), consumer).build();
58+
}
59+
60+
@Override
61+
protected List<Biome> getElements() {
62+
// A mutable list is required: UnifiedMultiSelector's constructor sorts it in place.
63+
return StreamSupport.stream(Registry.BIOME.spliterator(), false)
64+
.filter(biome -> excluded == null || !excluded.contains(biome))
65+
.sorted(Comparator.comparing(MultiBiomeSelector::biomeKey))
66+
.collect(Collectors.toCollection(ArrayList::new));
67+
}
68+
69+
@Override
70+
protected String getTitleKey() {
71+
return "biome-selector";
72+
}
73+
74+
@Override
75+
protected String getElementKeyPrefix() {
76+
return "biome.";
77+
}
78+
79+
@Override
80+
protected String getElementPlaceholder() {
81+
return "[biome]";
82+
}
83+
84+
@Override
85+
protected ItemStack getIcon(Biome element) {
86+
return new ItemStack(iconMaterial(biomeKey(element)));
87+
}
88+
89+
@Override
90+
protected String getElementDisplayName(Biome element) {
91+
return Utils.prettifyBiome(biomeKey(element));
92+
}
93+
94+
@Override
95+
protected String elementToString(Biome element) {
96+
return biomeKey(element);
97+
}
98+
99+
/**
100+
* Returns the namespaced key string (e.g. "minecraft:plains") for a biome.
101+
*
102+
* @param biome the biome.
103+
* @return its namespaced key.
104+
*/
105+
public static String biomeKey(Biome biome) {
106+
return biome.getKey().toString();
107+
}
108+
109+
/**
110+
* Picks a rough representative icon for a biome key. Biomes have no natural item, so this
111+
* is only a visual hint; the biome name in the button title is what identifies it.
112+
*
113+
* @param key the biome key.
114+
* @return a Material to use as the button icon.
115+
*/
116+
private static Material iconMaterial(String key) {
117+
String path = key.contains(":") ? key.substring(key.indexOf(':') + 1) : key;
118+
119+
if (path.contains("nether") || path.contains("basalt") || path.contains("soul") || path.contains("crimson")
120+
|| path.contains("warped")) {
121+
return Material.NETHERRACK;
122+
}
123+
if (path.contains("end")) {
124+
return Material.END_STONE;
125+
}
126+
if (path.contains("ocean") || path.contains("river")) {
127+
return Material.WATER_BUCKET;
128+
}
129+
if (path.contains("desert") || path.contains("beach") || path.contains("badlands")) {
130+
return Material.SAND;
131+
}
132+
if (path.contains("snow") || path.contains("frozen") || path.contains("ice") || path.contains("cold")) {
133+
return Material.SNOW_BLOCK;
134+
}
135+
if (path.contains("cave") || path.contains("deep") || path.contains("lush")) {
136+
return Material.STONE;
137+
}
138+
if (path.contains("mushroom")) {
139+
return Material.RED_MUSHROOM_BLOCK;
140+
}
141+
142+
return Material.GRASS_BLOCK;
143+
}
144+
}

src/main/java/world/bentobox/challenges/tasks/TryToComplete.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,21 @@ private ChallengeResult checkSurrounding(int factor)
12511251
return emptyResult;
12521252
}
12531253

1254+
// Check biome requirement: the player must be standing in one of the required biomes.
1255+
Set<String> requiredBiomes = this.getIslandRequirements().getRequiredBiomes();
1256+
1257+
if (!requiredBiomes.isEmpty())
1258+
{
1259+
String currentBiome = this.user.getLocation().getBlock().getBiome().getKey().toString();
1260+
1261+
if (!requiredBiomes.contains(currentBiome))
1262+
{
1263+
Utils.sendMessage(this.user, this.world, Constants.ERRORS + "wrong-biome",
1264+
"[biome]", Utils.prettifyBiome(currentBiome));
1265+
return emptyResult;
1266+
}
1267+
}
1268+
12541269
// Init location in player position.
12551270
BoundingBox boundingBox = this.user.getPlayer().getBoundingBox().clone();
12561271

0 commit comments

Comments
 (0)