Adds obstacle blocks for parkour

This change reverts the advanced hit-box detection for kill plane blocks, giving them a full block's hit-box again.
Instead, obstacle blocks have been added, which have an accurate hit-box, and can trigger a hit from any direction.
The horizontal kill plane hit box option has been removed as it's no longer useful.
This commit is contained in:
Kristian Knarvik 2023-10-02 23:05:39 +02:00
parent fc6bd33e87
commit 310802b42d
10 changed files with 216 additions and 80 deletions

View File

@ -213,8 +213,8 @@ These are all the options that can be changed for an arena.
| winLocation | The location players must reach to win the arena (see spawnLocation for valid values). If set, this overrides, and is used instead of, the win block type. |
| checkpointAdd | Adds a new checkpoint to the arena's checkpoints (see spawnLocation for valid values). |
| checkpointClear | Clears all current checkpoints. Give any value to execute. If not given a value, current checkpoints are shown. |
| killPlaneBlocks | A comma-separated list of materials which will force a loss on hit. +WOOL and other [material tags](#notes-about-material-tags) are supported as well. |
| horizontalKillPlaneHitBox | The number of blocks away kill plane blocks will trigger horizontally (0-1). This only affects things less than 1 block wide, such as horizontal lightning and end rods. |
| killPlaneBlocks | A comma-separated list of materials which will force a loss when stepped on. +WOOL and other [material tags](#notes-about-material-tags) are supported as well. |
| obstacleBlocks | A comma-separated list of materials which will force a loss when touched from any direction. +WOOL and other [material tags](#notes-about-material-tags) are supported as well. |
## Configuration options
@ -249,6 +249,7 @@ These are all the options that can be changed for an arena.
| mustDoGroupedInSequence | true/false | true | Whether grouped dropper arenas must be played in the correct sequence |
| ignoreRecordsUntilGroupBeatenOnce | true/false | false | Whether records won't be registered unless the player has already beaten all arenas in a group. That means players are required to do a second play-through to register a record for a grouped arena. |
| killPlaneBlocks | list | [see this](#killplaneblocks-default) | The types of blocks compromising parkour arenas' kill planes. Add any materials you want to use for the "bottom" of your parkour arenas. +WOOL and other [material tags](#notes-about-material-tags) are supported. |
| obstacleBlocks | list | [see this](#obstacleblocks-default) | The types of blocks treated as obstacles in every direction. +WOOL and other [material tags](#notes-about-material-tags) are supported. |
#### blockWhitelist default:
@ -270,6 +271,12 @@ These are all the options that can be changed for an arena.
- LAVA
- MAGMA_BLOCK
#### obstacleBlocks default:
- END_ROD
- LIGHTNING_ROD
- CHAIN
## Record placeholders
Player records can be displayed on a leaderboard by using PlaceholderAPI. If you want to display a sign-based

View File

@ -74,9 +74,14 @@ public class ParkourArena implements Arena {
private @Nullable Set<Material> killPlaneBlocks;
/**
* The number of horizontal blocks the hit-box of kill plane blocks should cover
* The names of the block types serving as obstacles for this arena
*/
private double horizontalKillPlaneHitBox;
private @Nullable Set<String> obstacleBlockNames;
/**
* The block types serving as obstacles for this arena
*/
private @Nullable Set<Material> obstacleBlocks;
/**
* The checkpoints for this arena. Entering a checkpoint overrides the player's spawn location.
@ -101,8 +106,8 @@ public class ParkourArena implements Arena {
* @param exitLocation <p>The location the players are teleported to when exiting the arena, or null</p>
* @param winBlockType <p>The material of the block players have to hit to win this parkour arena</p>
* @param winLocation <p>The location a player has to reach to win this arena</p>
* @param killPlaneBlockNames <p>The names of the type of blocks</p>
* @param horizontalKillPlaneHitBox <p>The number of horizontal blocks the hit-box of kill plane blocks should cover</p>
* @param killPlaneBlockNames <p>The names of the types of blocks that trigger a loss when stepped on</p>
* @param obstacleBlockNames <p>The names of the types of blocks that trigger a loss when touched</p>
* @param checkpoints <p>The checkpoints set for this arena</p>
* @param rewards <p>The rewards given by this arena</p>
* @param parkourArenaData <p>The arena data keeping track of which players have done what in this arena</p>
@ -110,7 +115,7 @@ public class ParkourArena implements Arena {
*/
public ParkourArena(@NotNull UUID arenaId, @NotNull String arenaName, @NotNull Location spawnLocation,
@Nullable Location exitLocation, @NotNull Material winBlockType, @Nullable Location winLocation,
@Nullable Set<String> killPlaneBlockNames, double horizontalKillPlaneHitBox,
@Nullable Set<String> killPlaneBlockNames, @Nullable Set<String> obstacleBlockNames,
@NotNull List<Location> checkpoints,
@NotNull Map<RewardCondition, Set<Reward>> rewards,
@NotNull ParkourArenaData parkourArenaData, @NotNull ParkourArenaHandler arenaHandler) {
@ -123,7 +128,9 @@ public class ParkourArena implements Arena {
this.killPlaneBlockNames = killPlaneBlockNames;
this.killPlaneBlocks = this.killPlaneBlockNames == null ? null : MaterialHelper.loadMaterialList(
new ArrayList<>(killPlaneBlockNames), "+", MiniGames.getInstance().getLogger());
this.horizontalKillPlaneHitBox = horizontalKillPlaneHitBox;
this.obstacleBlockNames = obstacleBlockNames;
this.obstacleBlocks = this.obstacleBlockNames == null ? null : MaterialHelper.loadMaterialList(
new ArrayList<>(obstacleBlockNames), "+", MiniGames.getInstance().getLogger());
this.checkpoints = checkpoints;
this.parkourArenaData = parkourArenaData;
this.parkourArenaHandler = arenaHandler;
@ -156,9 +163,9 @@ public class ParkourArena implements Arena {
this.parkourArenaData = new ParkourArenaData(this.arenaId, recordRegistries, new HashMap<>());
this.winBlockType = Material.EMERALD_BLOCK;
this.killPlaneBlocks = null;
this.obstacleBlocks = null;
this.checkpoints = new ArrayList<>();
this.parkourArenaHandler = arenaHandler;
this.horizontalKillPlaneHitBox = 0.1;
}
@Override
@ -251,34 +258,25 @@ public class ParkourArena implements Arena {
}
/**
* Gets the number of horizontal blocks the hit-box of kill plane blocks should cover
* Gets the block types used for this parkour arena's obstacle blocks
*
* <p>This is kind of hard to explain, but basically, when the player is less than the specified amount of blocks
* away from a kill plane block horizontally, a fail will be triggered. Sane values for this would usually be
* between 0.01 amd 1.</p>
*
* @return <p>The number of horizontal blocks the hit-box of kill plane blocks should cover</p>
* @return <p>The types of blocks used as obstacles</p>
*/
public double getHorizontalKillPlaneHitBox() {
return this.horizontalKillPlaneHitBox;
public @NotNull Set<Material> getObstacleBlocks() {
if (this.obstacleBlocks != null) {
return new HashSet<>(this.obstacleBlocks);
} else {
return MiniGames.getInstance().getParkourConfiguration().getObstacleBlocks();
}
}
/**
* Sets the number of horizontal blocks the hit-box of kill plane blocks should cover
* Gets the names of the blocks used as this arena's obstacle blocks
*
* <p>This is kind of hard to explain, but basically, when the player is less than the specified amount of blocks
* away from a kill plane block horizontally, a fail will be triggered. Sane values for this would usually be
* between 0.01 amd 1.</p>
*
* @param horizontalKillPlaneHitBox <p>The number of horizontal blocks the hit-box of kill plane blocks should cover</p>
* @return <p>The names of the blocks used as this arena's obstacle blocks</p>
*/
public boolean setHorizontalKillPlaneHitBox(double horizontalKillPlaneHitBox) {
if (horizontalKillPlaneHitBox > 1 || horizontalKillPlaneHitBox < -1) {
return false;
}
this.horizontalKillPlaneHitBox = horizontalKillPlaneHitBox;
this.saveArena();
return true;
public @Nullable Set<String> getObstacleBlockNames() {
return this.obstacleBlockNames;
}
/**
@ -335,7 +333,7 @@ public class ParkourArena implements Arena {
@Override
public boolean willCauseLoss(Block block) {
return this.getKillPlaneBlocks().contains(block.getType());
return this.getKillPlaneBlocks().contains(block.getType()) || this.getObstacleBlocks().contains(block.getType());
}
@Override
@ -451,6 +449,28 @@ public class ParkourArena implements Arena {
return true;
}
/**
* Sets the type of blocks used as obstacle blocks
*
* @param obstacleBlockNames <p>The names of the obstacle blocks</p>
*/
public boolean setObstacleBlocks(@NotNull Set<String> obstacleBlockNames) {
if (obstacleBlockNames.isEmpty()) {
this.obstacleBlockNames = null;
this.obstacleBlocks = null;
} else {
Set<Material> parsed = MaterialHelper.loadMaterialList(new ArrayList<>(obstacleBlockNames), "+",
MiniGames.getInstance().getLogger());
if (parsed.isEmpty()) {
return false;
}
this.obstacleBlockNames = obstacleBlockNames;
this.obstacleBlocks = parsed;
}
this.saveArena();
return true;
}
/**
* Adds a checkpoint to this arena
*

View File

@ -64,10 +64,12 @@ public enum ParkourArenaEditableProperty {
EditablePropertyType.MATERIAL_LIST),
/**
* The horizontal hit-box of kill blocks
* The blocks used as this arena's obstacle blocks
*/
HORIZONTAL_KILL_PLANE_HIT_BOX("horizontalKillPlaneHitBox",
(arena) -> String.valueOf(arena.getHorizontalKillPlaneHitBox()), EditablePropertyType.DOUBLE);
OBSTACLE_BLOCKS("obstacleBlocks", (arena) -> String.valueOf(arena.getObstacleBlockNames()),
EditablePropertyType.MATERIAL_LIST),
;
private final @NotNull String argumentString;
private final Function<ParkourArena, String> currentValueProvider;

View File

@ -43,9 +43,9 @@ public enum ParkourArenaStorageKey {
KILL_PLANE_BLOCKS("killPlaneBlocks"),
/**
* The key for this arena's horizontal kill plane hit box
* The key for this arena's obstacle blocks (overrides the config)
*/
HORIZONTAL_KILL_PLANE_HIT_BOX("horizontalKillPlaneHitBox"),
OBSTACLE_BLOCKS("obstacleBlocks"),
/**
* The key for this arena's checkpoint locations

View File

@ -98,7 +98,7 @@ public class EditParkourArenaCommand implements CommandExecutor {
case CHECKPOINT_ADD -> arena.addCheckpoint(parseLocation(player, value));
case CHECKPOINT_CLEAR -> arena.clearCheckpoints();
case KILL_PLANE_BLOCKS -> arena.setKillPlaneBlocks(new HashSet<>(List.of(value.split(","))));
case HORIZONTAL_KILL_PLANE_HIT_BOX -> arena.setHorizontalKillPlaneHitBox(Double.parseDouble(value));
case OBSTACLE_BLOCKS -> arena.setObstacleBlocks(new HashSet<>(List.of(value.split(","))));
};
}

View File

@ -17,6 +17,7 @@ public class ParkourConfiguration extends MiniGameConfiguration {
private boolean mustDoGroupedInSequence;
private boolean ignoreRecordsUntilGroupBeatenOnce;
private Set<Material> killPlaneBlocks;
private Set<Material> obstacleBlocks;
/**
* Instantiates a new dropper configuration
@ -63,12 +64,22 @@ public class ParkourConfiguration extends MiniGameConfiguration {
return new HashSet<>(this.killPlaneBlocks);
}
/**
* Gets all types of blocks constituting parkour arena's obstacle blocks
*
* @return <p>The types of blocks constituting parkour arena's obstacle blocks</p>
*/
public Set<Material> getObstacleBlocks() {
return new HashSet<>(this.obstacleBlocks);
}
@Override
protected void load() {
this.enforceCheckpointOrder = configuration.getBoolean(rootNode + "enforceCheckpointOrder", false);
this.mustDoGroupedInSequence = configuration.getBoolean(rootNode + "mustDoGroupedInSequence", true);
this.ignoreRecordsUntilGroupBeatenOnce = configuration.getBoolean(rootNode + "ignoreRecordsUntilGroupBeatenOnce", false);
this.killPlaneBlocks = loadMaterialList(rootNode + "killPlaneBlocks");
this.obstacleBlocks = loadMaterialList(rootNode + "obstacleBlocks");
}
@Override
@ -82,6 +93,10 @@ public class ParkourConfiguration extends MiniGameConfiguration {
for (Material material : killPlaneBlocks) {
builder.append("\n - ").append(material.name());
}
builder.append("\n" + "Obstacle blocks: ");
for (Material material : obstacleBlocks) {
builder.append("\n - ").append(material.name());
}
return builder.toString();
}

View File

@ -15,6 +15,7 @@ import net.knarcraft.minigames.config.ParkourConfiguration;
import net.knarcraft.minigames.config.SharedConfiguration;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@ -34,6 +35,8 @@ import java.util.Set;
*/
public class MoveListener implements Listener {
private static final BoundingBox fullBlockBox = new BoundingBox(0, 0, 0, 1, 1, 1);
private final DropperConfiguration dropperConfiguration;
private final ParkourConfiguration parkourConfiguration;
@ -51,12 +54,15 @@ public class MoveListener implements Listener {
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
// Ignore if no actual movement is happening
if (event.getFrom().equals(event.getTo()) || event.getTo() == null) {
if (event.getTo() == null) {
return;
}
ArenaSession session = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId());
if (session instanceof DropperArenaSession dropperSession) {
if (event.getFrom().equals(event.getTo())) {
return;
}
doDropperArenaChecks(event, dropperSession);
} else if (session instanceof ParkourArenaSession parkourSession) {
doParkourArenaChecks(event, parkourSession);
@ -70,8 +76,7 @@ public class MoveListener implements Listener {
* @param arenaSession <p>The dropper session of the player triggering the event</p>
*/
private void doParkourArenaChecks(@NotNull PlayerMoveEvent event, ParkourArenaSession arenaSession) {
// Ignore movement which won't cause the player's block to change
if (event.getTo() == null || isSameLocation(event.getFrom(), event.getTo())) {
if (event.getTo() == null) {
return;
}
@ -228,7 +233,7 @@ public class MoveListener implements Listener {
}
}
} else if (arena instanceof ParkourArena) {
return checkParkourDeathBlock(arenaSession, toLocation);
return checkParkourDeathBlock((ParkourArenaSession) arenaSession, toLocation);
}
return false;
@ -241,36 +246,50 @@ public class MoveListener implements Listener {
* @param toLocation <p>The location the player is moving to</p>
* @return <p>True if the player hit a death block</p>
*/
private boolean checkParkourDeathBlock(@NotNull ArenaSession arenaSession,
private boolean checkParkourDeathBlock(@NotNull ParkourArenaSession arenaSession,
@NotNull Location toLocation) {
// If the player is standing on a non-full block, event.getTo will give the correct block, but if not, the
// block below has to be checked instead.
Set<Block> blocksBelow = getBlocksBeneathLocation(toLocation, 0);
Set<Block> adjustedBlocks = new HashSet<>();
for (Block block : blocksBelow) {
if (block.getType().isAir()) {
block = block.getLocation().clone().subtract(0, 0.2, 0).getBlock();
// Only trigger hit detection for passable blocks if the player is in the block
if (block.isPassable()) {
continue;
}
}
if (arenaSession.getArena().willCauseLoss(block)) {
adjustedBlocks.add(block);
// A simple check, only for kill blocks
if (isOnKillBlock(arenaSession, toLocation)) {
return true;
}
// As the check for obstacle blocks is extensive, it's skipped if possible
Set<Material> obstacleBlocks = arenaSession.getArena().getObstacleBlocks();
if (obstacleBlocks.isEmpty()) {
return false;
}
// Create a hit-box approximate to the player's real hit-box
double horizontalHitBox = ((ParkourArena) arenaSession.getArena()).getHorizontalKillPlaneHitBox();
BoundingBox playerBox = new BoundingBox(-horizontalHitBox, -0.1, -horizontalHitBox,
0.6 + horizontalHitBox, 1, 0.6 + horizontalHitBox).shift(
double playerHeight = 1.8;
Player player = Bukkit.getPlayer(arenaSession.getEntryState().getPlayerId());
if (player != null && player.isSneaking()) {
playerHeight = 1.5;
}
BoundingBox playerBox = new BoundingBox(-0.05, -0.05, -0.05,
0.6 + 0.05, playerHeight + 0.05, 0.6 + 0.05).shift(
toLocation).shift(-0.3, -0.05, -0.3);
BoundingBox playerPassableBox = new BoundingBox(0.2, 0.5, 0.2,
0.4, playerHeight - 0.5, 0.4).shift(
toLocation).shift(-0.3, 0, -0.3);
for (Block block : adjustedBlocks) {
// For liquids, or anything without a proper collision shape, trigger collision
Set<Block> possiblyHitBlocks = new HashSet<>();
possiblyHitBlocks.addAll(getBlocksBeneathLocation(toLocation, 0, 0.01));
possiblyHitBlocks.addAll(getBlocksBeneathLocation(toLocation, 1, 0.01));
possiblyHitBlocks.addAll(getBlocksBeneathLocation(toLocation, -1, 0.01));
possiblyHitBlocks.addAll(getBlocksBeneathLocation(toLocation, -2, 0.01));
for (Block block : possiblyHitBlocks) {
if (!obstacleBlocks.contains(block.getType())) {
continue;
}
// For liquids, or anything without a proper collision shape, trigger collision if the player is partly
// inside when treated as a full block
if (block.isLiquid() || block.getCollisionShape().getBoundingBoxes().isEmpty()) {
if (playerPassableBox.overlaps(fullBlockBox.clone().shift(block.getLocation()))) {
arenaSession.triggerLoss();
return true;
}
}
// Check whether the player's actual hit-box is intersecting with a block
for (BoundingBox boundingBox : block.getCollisionShape().getBoundingBoxes()) {
@ -286,6 +305,34 @@ public class MoveListener implements Listener {
return false;
}
/**
* As simple check for whether a player is moving on top of a kill block
*
* @param arenaSession <p>The arena session the player is in</p>
* @param toLocation <p>The location the player is moving to</p>
* @return <p>True if the player is on a kill block, and a loss has been triggered</p>
*/
private boolean isOnKillBlock(ParkourArenaSession arenaSession, Location toLocation) {
// If the player is standing on a non-full block, event.getTo will give the correct block, but if not, the
// block below has to be checked instead.
Set<Block> blocksBelow = getBlocksBeneathLocation(toLocation, 0);
Set<Material> killPlaneBlocks = arenaSession.getArena().getKillPlaneBlocks();
for (Block block : blocksBelow) {
if (block.getType().isAir()) {
block = block.getLocation().clone().subtract(0, 0.2, 0).getBlock();
// Only trigger hit detection for passable blocks if the player is in the block
if (block.isPassable()) {
continue;
}
}
if (killPlaneBlocks.contains(block.getType())) {
arenaSession.triggerLoss();
return true;
}
}
return false;
}
/**
* Gets the blocks at the given location that will be affected by the player's hit-box
*
@ -293,12 +340,31 @@ public class MoveListener implements Listener {
* @return <p>The blocks beneath the player</p>
*/
private Set<Block> getBlocksBeneathLocation(Location location, double depth) {
return getBlocksBeneathLocation(location, depth, 0);
}
/**
* Gets the blocks at the given location that will be affected by the player's hit-box
*
* @param location <p>The location to check</p>
* @param extraRange <p>Extra range of the square used for finding blocks</p>
* @return <p>The blocks beneath the player</p>
*/
private Set<Block> getBlocksBeneathLocation(Location location, double depth, double extraRange) {
Set<Block> blocksBeneath = new HashSet<>();
double halfPlayerWidth = 0.3;
double halfPlayerWidth = 0.3 + extraRange;
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, -halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, -halfPlayerWidth).getBlock());
// Once a certain size is reached, if the player is in the centre of a block, 9 must be accounted for
if (halfPlayerWidth > 0.5) {
blocksBeneath.add(location.getBlock());
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, 0).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, 0).getBlock());
blocksBeneath.add(location.clone().subtract(0, depth, -halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(0, depth, halfPlayerWidth).getBlock());
}
return blocksBeneath;
}

View File

@ -135,7 +135,7 @@ public final class ParkourArenaStorageHelper {
configSection.set(ParkourArenaStorageKey.WIN_BLOCK_TYPE.getKey(), new SerializableMaterial(arena.getWinBlockType()));
configSection.set(ParkourArenaStorageKey.WIN_LOCATION.getKey(), arena.getWinLocation());
configSection.set(ParkourArenaStorageKey.KILL_PLANE_BLOCKS.getKey(), getKillPlaneBlocks(arena));
configSection.set(ParkourArenaStorageKey.HORIZONTAL_KILL_PLANE_HIT_BOX.getKey(), arena.getHorizontalKillPlaneHitBox());
configSection.set(ParkourArenaStorageKey.OBSTACLE_BLOCKS.getKey(), getObstacleBlocks(arena));
configSection.set(ParkourArenaStorageKey.CHECKPOINTS.getKey(), arena.getCheckpoints());
RewardStorageHelper.saveRewards(arena, configSection, ParkourArenaStorageKey.REWARDS.getKey());
saveParkourArenaData(arena.getData());
@ -155,6 +155,20 @@ public final class ParkourArenaStorageHelper {
}
}
/**
* Gets a list of the obstacle blocks for the given arena
*
* @param arena <p>The arena to get obstacle blocks for</p>
* @return <p>The obstacle blocks</p>
*/
private static List<String> getObstacleBlocks(ParkourArena arena) {
if (arena.getObstacleBlockNames() == null) {
return new ArrayList<>();
} else {
return new ArrayList<>(arena.getObstacleBlockNames());
}
}
/**
* Loads all arenas
*
@ -205,12 +219,17 @@ public final class ParkourArenaStorageHelper {
List<?> killPlaneBlockNamesList = configurationSection.getList(ParkourArenaStorageKey.KILL_PLANE_BLOCKS.getKey());
Set<String> killPlaneBlockNames;
if (killPlaneBlockNamesList == null) {
killPlaneBlockNames = new HashSet<>();
killPlaneBlockNames = null;
} else {
killPlaneBlockNames = new HashSet<>((List<String>) killPlaneBlockNamesList);
}
double horizontalKillPlaneHitBox = configurationSection.getDouble(
ParkourArenaStorageKey.HORIZONTAL_KILL_PLANE_HIT_BOX.getKey(), 0);
List<?> obstacleBlockNamesList = configurationSection.getList(ParkourArenaStorageKey.OBSTACLE_BLOCKS.getKey());
Set<String> obstacleBlockNames;
if (obstacleBlockNamesList == null) {
obstacleBlockNames = null;
} else {
obstacleBlockNames = new HashSet<>((List<String>) obstacleBlockNamesList);
}
List<Location> checkpoints = (List<Location>) configurationSection.get(ParkourArenaStorageKey.CHECKPOINTS.getKey());
Map<RewardCondition, Set<Reward>> rewards = RewardStorageHelper.loadRewards(configurationSection,
@ -242,7 +261,7 @@ public final class ParkourArenaStorageHelper {
}
return new ParkourArena(arenaId, arenaName, spawnLocation, exitLocation, winBlockType.getRawValue(), winLocation,
killPlaneBlockNames, horizontalKillPlaneHitBox, checkpoints, rewards, arenaData,
killPlaneBlockNames, obstacleBlockNames, checkpoints, rewards, arenaData,
MiniGames.getInstance().getParkourArenaHandler());
}

View File

@ -137,6 +137,7 @@ public final class TabCompleteHelper {
List<String> suggestions = new ArrayList<>();
suggestions.add("LAVA,MAGMA_BLOCK");
suggestions.add("WATER,MAGMA_BLOCK,LAVA,+BUTTONS,+CORALS");
suggestions.add("CHAIN,END_ROD,LIGHTNING_ROD");
return suggestions;
}

View File

@ -20,6 +20,12 @@ parkour:
killPlaneBlocks:
- LAVA
- MAGMA_BLOCK
# The blocks treated as obstacles in a parkour arena, which will trigger a loss in any direction
obstacleBlocks:
- END_ROD
- LIGHTNING_ROD
- CHAIN
dropper:
# Whether to block using the shift key to drop faster than the intended drop speed
blockSneaking: true