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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.bukkit.Fluid;
import org.bukkit.Material;
Expand Down Expand Up @@ -79,6 +80,15 @@ public IslandRequirements() {
@Expose
private int searchRadius = 10;

/**
* Namespaced keys (e.g. "minecraft:plains") of biomes the player must be standing in to
* complete the challenge. Stored as key strings rather than Biome objects so serialization
* is version-robust (Biome is a registry-backed type) and unknown biomes simply never match.
* Empty means no biome restriction.
*/
@Expose
private Set<String> requiredBiomes = new HashSet<>();

// ---------------------------------------------------------------------
// Section: Getters and Setters
// ---------------------------------------------------------------------
Expand Down Expand Up @@ -182,6 +192,25 @@ public void setSearchRadius(int searchRadius) {
this.searchRadius = searchRadius;
}


/**
* @return the namespaced keys of biomes the player must stand in (empty for no restriction).
*/
public Set<String> getRequiredBiomes() {
if (this.requiredBiomes == null) {
this.requiredBiomes = new HashSet<>();
}
return requiredBiomes;
}


/**
* @param requiredBiomes the required biome keys to set.
*/
public void setRequiredBiomes(Set<String> requiredBiomes) {
this.requiredBiomes = requiredBiomes;
}

/**
* Method isValid returns if given requirement data is valid or not.
*
Expand Down Expand Up @@ -215,6 +244,7 @@ public Requirements copy() {
clone.setRemoveEntities(this.removeEntities);

clone.setSearchRadius(this.searchRadius);
clone.setRequiredBiomes(new HashSet<>(this.getRequiredBiomes()));

return clone;
}
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/world/bentobox/challenges/panel/CommonPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,17 @@ private String generateIslandChallenge(IslandRequirements requirement) {
entities.insert(0, this.user.getTranslationOrNothing(reference + "entities-title"));
}

// Required biomes
StringBuilder biomes = new StringBuilder();
if (!requirement.getRequiredBiomes().isEmpty()) {
biomes.append(this.user.getTranslationOrNothing(reference + "biomes-title"));
requirement.getRequiredBiomes().stream().sorted().forEach(biomeKey -> {
biomes.append("\n");
biomes.append(this.user.getTranslationOrNothing(reference + "biome-value", "[biome]",
Utils.prettifyBiome(biomeKey)));
});
}

String searchRadius = this.user.getTranslationOrNothing(reference + "search-radius", Constants.PARAMETER_NUMBER,
String.valueOf(requirement.getSearchRadius()));

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.World;
import org.bukkit.block.Biome;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.ItemStack;
Expand All @@ -35,6 +39,7 @@
import world.bentobox.challenges.panel.ConversationUtils;
import world.bentobox.challenges.panel.util.EnvironmentSelector;
import world.bentobox.challenges.panel.util.ItemSelector;
import world.bentobox.challenges.panel.util.MultiBiomeSelector;
import world.bentobox.challenges.panel.util.MultiBlockSelector;
import world.bentobox.challenges.utils.Constants;
import world.bentobox.challenges.utils.Utils;
Expand Down Expand Up @@ -190,6 +195,7 @@ private void buildIslandRequirementsPanel(PanelBuilder panelBuilder) {


panelBuilder.item(23, this.createRequirementButton(RequirementButton.SEARCH_RADIUS));
panelBuilder.item(24, this.createRequirementButton(RequirementButton.REQUIRED_BIOMES));
panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS));
}

Expand Down Expand Up @@ -740,6 +746,11 @@ private PanelItem createRequirementButton(RequirementButton button) {
REQUIRED_MATERIALTAGS, REQUIRED_ENTITYTAGS -> {
return this.createIslandRequirementButton(button);
}
// Biome requirement is a self-contained island requirement, kept out of the large
// createIslandRequirementButton switch.
case REQUIRED_BIOMES -> {
return this.createBiomeRequirementButton();
}
// Buttons for Inventory Requirements
case REQUIRED_ITEMS, REMOVE_ITEMS, ADD_IGNORED_META, REMOVE_IGNORED_META -> {
return this.createInventoryRequirementButton(button);
Expand Down Expand Up @@ -933,6 +944,68 @@ private PanelItem createIslandRequirementButton(RequirementButton button) {
.clickHandler(clickHandler).build();
}


/**
* Creates the "Required Biomes" button for island challenges. Left-click opens the biome
* selector (hiding already-chosen biomes); right-click clears the list.
*
* @return the PanelItem for the biome requirement button.
*/
private PanelItem createBiomeRequirementButton() {
final String reference = Constants.BUTTON + RequirementButton.REQUIRED_BIOMES.name().toLowerCase() + ".";
final String name = this.user.getTranslation(reference + "name");
final IslandRequirements requirements = this.challenge.getRequirements();
final List<String> description = new ArrayList<>();
description.add(this.user.getTranslation(reference + Constants.DESCRIPTION_KEY));

if (requirements.getRequiredBiomes().isEmpty()) {
description.add(this.user.getTranslation(reference + "none"));
} else {
description.add(this.user.getTranslation(reference + Constants.TITLE_KEY));
requirements.getRequiredBiomes().forEach(biomeKey -> description.add(
this.user.getTranslation(reference + "list", "[biome]", Utils.prettifyBiome(biomeKey))));
}

description.add("");
description.add(this.user.getTranslation(Constants.CLICK_TO_ADD));

if (!requirements.getRequiredBiomes().isEmpty()) {
description.add(this.user.getTranslation(Constants.TIPS + "right-click-to-clear"));
}

return new PanelItemBuilder().icon(new ItemStack(Material.GRASS_BLOCK)).name(name).description(description)
.glow(false).clickHandler((panel, user, clickType, slot) -> {
if (clickType.isRightClick()) {
requirements.getRequiredBiomes().clear();
this.build();
} else {
this.openBiomeSelector(requirements);
}
return true;
}).build();
}


/**
* Opens the biome selector for the given island requirements, excluding already-chosen
* biomes, and adds the chosen biomes back to the requirement before rebuilding the panel.
*
* @param requirements the island requirements being edited.
*/
private void openBiomeSelector(IslandRequirements requirements) {
Set<Biome> excluded = requirements.getRequiredBiomes().stream()
.map(key -> Registry.BIOME.get(NamespacedKey.fromString(key)))
.filter(Objects::nonNull).collect(Collectors.toSet());

MultiBiomeSelector.open(this.user, excluded, (status, biomes) -> {
if (Boolean.TRUE.equals(status) && biomes != null) {
biomes.forEach(biome -> requirements.getRequiredBiomes().add(MultiBiomeSelector.biomeKey(biome)));
}

this.build();
});
}

/**
* This method creates buttons for inventory requirements menu.
*
Expand Down Expand Up @@ -1090,7 +1163,7 @@ private PanelItem createInventoryRequirementButton(RequirementButton button) {
glow = false;

description.add("");
description.add(this.user.getTranslation(Constants.TIPS + "click-to-add"));
description.add(this.user.getTranslation(Constants.CLICK_TO_ADD));
}
case REMOVE_IGNORED_META -> {
icon = new ItemStack(Material.RED_SHULKER_BOX);
Expand Down Expand Up @@ -1795,7 +1868,7 @@ private PanelItem createRewardButton(RewardButton button) {
glow = false;

description.add("");
description.add(this.user.getTranslation(Constants.TIPS + "click-to-add"));
description.add(this.user.getTranslation(Constants.CLICK_TO_ADD));
}
case REMOVE_IGNORED_META -> {
icon = new ItemStack(Material.RED_SHULKER_BOX);
Expand Down Expand Up @@ -1926,7 +1999,7 @@ public enum RequirementButton {
REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY, STATISTIC, STATISTIC_BLOCKS, STATISTIC_ITEMS,
STATISTIC_ENTITIES,
STATISTIC_AMOUNT, REMOVE_STATISTIC, REQUIRED_MATERIALTAGS, REQUIRED_ENTITYTAGS, REQUIRED_STATISTICS,
REMOVE_STATISTICS, REQUIRED_PAPI, REQUIRED_ADVANCEMENTS,
REMOVE_STATISTICS, REQUIRED_PAPI, REQUIRED_ADVANCEMENTS, REQUIRED_BIOMES,
}

// ---------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package world.bentobox.challenges.panel.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import org.bukkit.Material;
import org.bukkit.Registry;
import org.bukkit.block.Biome;
import org.bukkit.inventory.ItemStack;

import world.bentobox.bentobox.api.user.User;
import world.bentobox.challenges.utils.Utils;

/**
* A multi-selector GUI for choosing biomes. Extends the unified multi-selector base and
* supplies the biome-specific details (element list, icons, display names).
*
* <p>Biomes are a registry-backed type in modern Minecraft, so they are handled by their
* namespaced key rather than as an enum.
*/
public class MultiBiomeSelector extends UnifiedMultiSelector<Biome> {

private final Set<Biome> excluded;

private MultiBiomeSelector(User user, Set<Biome> excluded,
BiConsumer<Boolean, Collection<Biome>> consumer) {
super(user, Mode.ANY, consumer);
this.excluded = excluded;
}

/**
* Opens the biome selector.
*
* @param user the user who opens the GUI.
* @param excluded biomes to hide from the list (e.g. ones already selected).
* @param consumer callback receiving the confirmation flag and the chosen biomes.
*/
public static void open(User user, Set<Biome> excluded,
BiConsumer<Boolean, Collection<Biome>> consumer) {
new MultiBiomeSelector(user, excluded, consumer).build();
}

/**
* Opens the biome selector with no exclusions.
*
* @param user the user who opens the GUI.
* @param consumer callback receiving the confirmation flag and the chosen biomes.
*/
public static void open(User user, BiConsumer<Boolean, Collection<Biome>> consumer) {
new MultiBiomeSelector(user, new HashSet<>(), consumer).build();
}

@Override
protected List<Biome> getElements() {
// A mutable list is required: UnifiedMultiSelector's constructor sorts it in place.
return StreamSupport.stream(Registry.BIOME.spliterator(), false)
.filter(biome -> excluded == null || !excluded.contains(biome))
.sorted(Comparator.comparing(MultiBiomeSelector::biomeKey))
.collect(Collectors.toCollection(ArrayList::new));
}

@Override
protected String getTitleKey() {
return "biome-selector";
}

@Override
protected String getElementKeyPrefix() {
return "biome.";
}

@Override
protected String getElementPlaceholder() {
return "[biome]";
}

@Override
protected ItemStack getIcon(Biome element) {
return new ItemStack(iconMaterial(biomeKey(element)));
}

@Override
protected String getElementDisplayName(Biome element) {
return Utils.prettifyBiome(biomeKey(element));
}

@Override
protected String elementToString(Biome element) {
return biomeKey(element);
}

/**
* Returns the namespaced key string (e.g. "minecraft:plains") for a biome.
*
* @param biome the biome.
* @return its namespaced key.
*/
public static String biomeKey(Biome biome) {
return biome.getKey().toString();
}

/**
* Picks a rough representative icon for a biome key. Biomes have no natural item, so this
* is only a visual hint; the biome name in the button title is what identifies it.
*
* @param key the biome key.
* @return a Material to use as the button icon.
*/
private static Material iconMaterial(String key) {
String path = key.contains(":") ? key.substring(key.indexOf(':') + 1) : key;

if (path.contains("nether") || path.contains("basalt") || path.contains("soul") || path.contains("crimson")
|| path.contains("warped")) {
return Material.NETHERRACK;
}
if (path.contains("end")) {
return Material.END_STONE;
}
if (path.contains("ocean") || path.contains("river")) {
return Material.WATER_BUCKET;
}
if (path.contains("desert") || path.contains("beach") || path.contains("badlands")) {
return Material.SAND;
}
if (path.contains("snow") || path.contains("frozen") || path.contains("ice") || path.contains("cold")) {
return Material.SNOW_BLOCK;
}
if (path.contains("cave") || path.contains("deep") || path.contains("lush")) {
return Material.STONE;
}
if (path.contains("mushroom")) {
return Material.RED_MUSHROOM_BLOCK;
}

return Material.GRASS_BLOCK;
}
}
15 changes: 15 additions & 0 deletions src/main/java/world/bentobox/challenges/tasks/TryToComplete.java
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,21 @@ private ChallengeResult checkSurrounding(int factor)
return emptyResult;
}

// Check biome requirement: the player must be standing in one of the required biomes.
Set<String> requiredBiomes = this.getIslandRequirements().getRequiredBiomes();

if (!requiredBiomes.isEmpty())
{
String currentBiome = this.user.getLocation().getBlock().getBiome().getKey().toString();

if (!requiredBiomes.contains(currentBiome))
{
Utils.sendMessage(this.user, this.world, Constants.ERRORS + "wrong-biome",
"[biome]", Utils.prettifyBiome(currentBiome));
return emptyResult;
}
}

// Init location in player position.
BoundingBox boundingBox = this.user.getPlayer().getBoundingBox().clone();

Expand Down
Loading
Loading