15 Commits
v1.0 ... v1.1

Author SHA1 Message Date
b063bd1088 Bumps version for release 2023-12-29 01:56:05 +01:00
310802b42d 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.
2023-10-02 23:05:39 +02:00
fc6bd33e87 Adds ability to specify horizontal hit-box for end-rods and similar 2023-09-27 23:13:03 +02:00
ccf43060dc Adds exact hit-boxes for parkour kill planes 2023-09-27 19:14:18 +02:00
a498e9bad0 Fixes some liquid hitbox problems 2023-09-25 20:50:02 +02:00
d8bf77d317 Fixes visibility of some methods 2023-09-17 15:18:51 +02:00
162aff0c1f Fixes a bug in killBlockNames, and reduces writes 2023-09-17 08:03:03 +02:00
a7cfe36c72 Fixes parkour kill plane block names not being updated 2023-08-18 17:54:39 +02:00
407acf0ea2 Adds LIGHT to the block whitelist 2023-07-31 23:51:44 +02:00
b6bf22d1c7 Fixes incorrect alias for parkourRemove 2023-07-31 23:30:56 +02:00
67144fec06 Bumps version to SNAPSHOT again 2023-07-31 23:01:36 +02:00
e52732433a Fixes some incorrect command usage descriptions 2023-07-31 23:01:03 +02:00
776fc5a757 Fixes some markdown 2023-07-17 23:58:25 +02:00
1e7cdf02fc Makes possible player name placeholders more clear 2023-07-17 22:53:28 +02:00
f3d8be8be2 Adds missing info about the command reward player name placeholder 2023-07-17 18:26:27 +02:00
17 changed files with 477 additions and 87 deletions

View File

@ -99,7 +99,9 @@ This reward requires an argument which is the permission string you want to gran
###### Command ###### Command
The reward requires the command as an argument. Type the full command with spaces and everything, but omit the `/` at The reward requires the command as an argument. Type the full command with spaces and everything, but omit the `/` at
the beginning of the command. the beginning of the command. Use %player_name% or anything that matches the
RegEx `[<%(\\[{]player[_\\-]?(name)?[>%)\\]}]` (\<player>. \<player-name>, %player_name%, {player}, etc.) as the
placeholder for the rewarded player's name.
###### Item ###### Item
@ -202,16 +204,17 @@ This command allows editing the specified property for the specified parkour are
These are all the options that can be changed for an arena. These are all the options that can be changed for an arena.
| Option | Details | | Option | Details |
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | The name of the arena. Used mainly to select the arena in commands. Note that underscore (_) cannot be used if you want to utilize placeholders, as it's used to split placeholder arguments. | | name | The name of the arena. Used mainly to select the arena in commands. Note that underscore (_) cannot be used if you want to utilize placeholders, as it's used to split placeholder arguments. |
| spawnLocation | The spawn location of any player joining the arena. Use `56.546,64.0,44.45` to specify coordinates, or `here`, `this` or any other string to select your current location. | | spawnLocation | The spawn location of any player joining the arena. Use `56.546,64.0,44.45` to specify coordinates, or `here`, `this` or any other string to select your current location. |
| exitLocation | The location players will be sent to when exiting the arena. If not set, the player will be sent to where they joined from. Valid values are the same as for spawnLocation. | | exitLocation | The location players will be sent to when exiting the arena. If not set, the player will be sent to where they joined from. Valid values are the same as for spawnLocation. |
| winBlockType | The type of block players must hit to win the arena. It can be any material as long as it's a block, and not a type of air. | | winBlockType | The type of block players must hit to win the arena. It can be any material as long as it's a block, and not a type of air. |
| 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. | | 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). | | 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. | | 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. | | 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 ## Configuration options
@ -246,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 | | 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. | | 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. | | 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: #### blockWhitelist default:
@ -267,6 +271,12 @@ These are all the options that can be changed for an arena.
- LAVA - LAVA
- MAGMA_BLOCK - MAGMA_BLOCK
#### obstacleBlocks default:
- END_ROD
- LIGHTNING_ROD
- CHAIN
## Record placeholders ## Record placeholders
Player records can be displayed on a leaderboard by using PlaceholderAPI. If you want to display a sign-based Player records can be displayed on a leaderboard by using PlaceholderAPI. If you want to display a sign-based

View File

@ -6,7 +6,7 @@
<groupId>net.knarcraft</groupId> <groupId>net.knarcraft</groupId>
<artifactId>MiniGames</artifactId> <artifactId>MiniGames</artifactId>
<version>1.0</version> <version>1.1</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>MiniGames</name> <name>MiniGames</name>

View File

@ -38,6 +38,11 @@ public enum EditablePropertyType {
/** /**
* The property is a comma-separated list of materials * The property is a comma-separated list of materials
*/ */
MATERIAL_LIST MATERIAL_LIST,
/**
* The property is any double value
*/
DOUBLE,
} }

View File

@ -21,6 +21,7 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level;
import static net.knarcraft.minigames.util.InputValidationHelper.isInvalid; import static net.knarcraft.minigames.util.InputValidationHelper.isInvalid;
@ -167,13 +168,13 @@ public class DropperArena implements Arena {
public void addReward(@NotNull RewardCondition rewardCondition, @NotNull Reward reward) { public void addReward(@NotNull RewardCondition rewardCondition, @NotNull Reward reward) {
this.rewards.computeIfAbsent(rewardCondition, k -> new HashSet<>()); this.rewards.computeIfAbsent(rewardCondition, k -> new HashSet<>());
this.rewards.get(rewardCondition).add(reward); this.rewards.get(rewardCondition).add(reward);
this.dropperArenaHandler.saveArenas(); this.saveArena();
} }
@Override @Override
public void clearRewards(@NotNull RewardCondition rewardCondition) { public void clearRewards(@NotNull RewardCondition rewardCondition) {
this.rewards.remove(rewardCondition); this.rewards.remove(rewardCondition);
this.dropperArenaHandler.saveArenas(); this.saveArena();
} }
@Override @Override
@ -267,7 +268,7 @@ public class DropperArena implements Arena {
return false; return false;
} else { } else {
this.spawnLocation = newLocation; this.spawnLocation = newLocation;
this.dropperArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
} }
@ -283,7 +284,7 @@ public class DropperArena implements Arena {
return false; return false;
} else { } else {
this.exitLocation = newLocation; this.exitLocation = newLocation;
this.dropperArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
} }
@ -300,7 +301,7 @@ public class DropperArena implements Arena {
this.arenaName = arenaName; this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately // Update the arena lookup map to make sure the new name can be used immediately
this.dropperArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized()); this.dropperArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
this.dropperArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} else { } else {
return false; return false;
@ -320,7 +321,7 @@ public class DropperArena implements Arena {
return false; return false;
} else { } else {
this.winBlockType = material; this.winBlockType = material;
this.dropperArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
} }
@ -338,7 +339,7 @@ public class DropperArena implements Arena {
return false; return false;
} else { } else {
this.playerHorizontalVelocity = horizontalVelocity; this.playerHorizontalVelocity = horizontalVelocity;
this.dropperArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
} }
@ -354,11 +355,24 @@ public class DropperArena implements Arena {
return false; return false;
} else { } else {
this.playerVerticalVelocity = verticalVelocity; this.playerVerticalVelocity = verticalVelocity;
this.dropperArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
} }
/**
* Saves this arena to disk
*/
public void saveArena() {
try {
DropperArenaStorageHelper.saveSingleDropperArena(this);
} catch (IOException exception) {
MiniGames.log(Level.SEVERE, "Unable to save arena! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, exception.getMessage());
}
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
if (!(other instanceof DropperArena otherArena)) { if (!(other instanceof DropperArena otherArena)) {

View File

@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level;
import static net.knarcraft.minigames.util.InputValidationHelper.isInvalid; import static net.knarcraft.minigames.util.InputValidationHelper.isInvalid;
@ -72,6 +73,16 @@ public class ParkourArena implements Arena {
*/ */
private @Nullable Set<Material> killPlaneBlocks; private @Nullable Set<Material> killPlaneBlocks;
/**
* The names of the block types serving as obstacles for this arena
*/
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. * The checkpoints for this arena. Entering a checkpoint overrides the player's spawn location.
*/ */
@ -95,7 +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 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 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 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 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 checkpoints <p>The checkpoints set for this arena</p>
* @param rewards <p>The rewards given by 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> * @param parkourArenaData <p>The arena data keeping track of which players have done what in this arena</p>
@ -103,7 +115,8 @@ public class ParkourArena implements Arena {
*/ */
public ParkourArena(@NotNull UUID arenaId, @NotNull String arenaName, @NotNull Location spawnLocation, public ParkourArena(@NotNull UUID arenaId, @NotNull String arenaName, @NotNull Location spawnLocation,
@Nullable Location exitLocation, @NotNull Material winBlockType, @Nullable Location winLocation, @Nullable Location exitLocation, @NotNull Material winBlockType, @Nullable Location winLocation,
@Nullable Set<String> killPlaneBlockNames, @NotNull List<Location> checkpoints, @Nullable Set<String> killPlaneBlockNames, @Nullable Set<String> obstacleBlockNames,
@NotNull List<Location> checkpoints,
@NotNull Map<RewardCondition, Set<Reward>> rewards, @NotNull Map<RewardCondition, Set<Reward>> rewards,
@NotNull ParkourArenaData parkourArenaData, @NotNull ParkourArenaHandler arenaHandler) { @NotNull ParkourArenaData parkourArenaData, @NotNull ParkourArenaHandler arenaHandler) {
this.arenaId = arenaId; this.arenaId = arenaId;
@ -115,6 +128,9 @@ public class ParkourArena implements Arena {
this.killPlaneBlockNames = killPlaneBlockNames; this.killPlaneBlockNames = killPlaneBlockNames;
this.killPlaneBlocks = this.killPlaneBlockNames == null ? null : MaterialHelper.loadMaterialList( this.killPlaneBlocks = this.killPlaneBlockNames == null ? null : MaterialHelper.loadMaterialList(
new ArrayList<>(killPlaneBlockNames), "+", MiniGames.getInstance().getLogger()); new ArrayList<>(killPlaneBlockNames), "+", MiniGames.getInstance().getLogger());
this.obstacleBlockNames = obstacleBlockNames;
this.obstacleBlocks = this.obstacleBlockNames == null ? null : MaterialHelper.loadMaterialList(
new ArrayList<>(obstacleBlockNames), "+", MiniGames.getInstance().getLogger());
this.checkpoints = checkpoints; this.checkpoints = checkpoints;
this.parkourArenaData = parkourArenaData; this.parkourArenaData = parkourArenaData;
this.parkourArenaHandler = arenaHandler; this.parkourArenaHandler = arenaHandler;
@ -147,6 +163,7 @@ public class ParkourArena implements Arena {
this.parkourArenaData = new ParkourArenaData(this.arenaId, recordRegistries, new HashMap<>()); this.parkourArenaData = new ParkourArenaData(this.arenaId, recordRegistries, new HashMap<>());
this.winBlockType = Material.EMERALD_BLOCK; this.winBlockType = Material.EMERALD_BLOCK;
this.killPlaneBlocks = null; this.killPlaneBlocks = null;
this.obstacleBlocks = null;
this.checkpoints = new ArrayList<>(); this.checkpoints = new ArrayList<>();
this.parkourArenaHandler = arenaHandler; this.parkourArenaHandler = arenaHandler;
} }
@ -180,13 +197,13 @@ public class ParkourArena implements Arena {
public void addReward(@NotNull RewardCondition rewardCondition, @NotNull Reward reward) { public void addReward(@NotNull RewardCondition rewardCondition, @NotNull Reward reward) {
this.rewards.computeIfAbsent(rewardCondition, k -> new HashSet<>()); this.rewards.computeIfAbsent(rewardCondition, k -> new HashSet<>());
this.rewards.get(rewardCondition).add(reward); this.rewards.get(rewardCondition).add(reward);
this.parkourArenaHandler.saveArenas(); this.saveArena();
} }
@Override @Override
public void clearRewards(@NotNull RewardCondition rewardCondition) { public void clearRewards(@NotNull RewardCondition rewardCondition) {
this.rewards.remove(rewardCondition); this.rewards.remove(rewardCondition);
this.parkourArenaHandler.saveArenas(); this.saveArena();
} }
@Override @Override
@ -240,6 +257,28 @@ public class ParkourArena implements Arena {
return this.killPlaneBlockNames; return this.killPlaneBlockNames;
} }
/**
* Gets the block types used for this parkour arena's obstacle blocks
*
* @return <p>The types of blocks used as obstacles</p>
*/
public @NotNull Set<Material> getObstacleBlocks() {
if (this.obstacleBlocks != null) {
return new HashSet<>(this.obstacleBlocks);
} else {
return MiniGames.getInstance().getParkourConfiguration().getObstacleBlocks();
}
}
/**
* Gets the names of the blocks used as this arena's obstacle blocks
*
* @return <p>The names of the blocks used as this arena's obstacle blocks</p>
*/
public @Nullable Set<String> getObstacleBlockNames() {
return this.obstacleBlockNames;
}
/** /**
* Gets all checkpoint locations for this arena * Gets all checkpoint locations for this arena
* *
@ -294,7 +333,7 @@ public class ParkourArena implements Arena {
@Override @Override
public boolean willCauseLoss(Block block) { public boolean willCauseLoss(Block block) {
return this.getKillPlaneBlocks().contains(block.getType()); return this.getKillPlaneBlocks().contains(block.getType()) || this.getObstacleBlocks().contains(block.getType());
} }
@Override @Override
@ -314,7 +353,7 @@ public class ParkourArena implements Arena {
return false; return false;
} else { } else {
this.spawnLocation = newLocation; this.spawnLocation = newLocation;
this.parkourArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
} }
@ -330,7 +369,7 @@ public class ParkourArena implements Arena {
return false; return false;
} else { } else {
this.exitLocation = newLocation; this.exitLocation = newLocation;
this.parkourArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
} }
@ -347,7 +386,7 @@ public class ParkourArena implements Arena {
this.arenaName = arenaName; this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately // Update the arena lookup map to make sure the new name can be used immediately
this.parkourArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized()); this.parkourArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
this.parkourArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} else { } else {
return false; return false;
@ -367,7 +406,7 @@ public class ParkourArena implements Arena {
return false; return false;
} else { } else {
this.winBlockType = material; this.winBlockType = material;
this.parkourArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
} }
@ -383,7 +422,7 @@ public class ParkourArena implements Arena {
return false; return false;
} else { } else {
this.winLocation = newLocation.clone(); this.winLocation = newLocation.clone();
this.parkourArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
} }
@ -395,6 +434,7 @@ public class ParkourArena implements Arena {
*/ */
public boolean setKillPlaneBlocks(@NotNull Set<String> killPlaneBlockNames) { public boolean setKillPlaneBlocks(@NotNull Set<String> killPlaneBlockNames) {
if (killPlaneBlockNames.isEmpty()) { if (killPlaneBlockNames.isEmpty()) {
this.killPlaneBlockNames = null;
this.killPlaneBlocks = null; this.killPlaneBlocks = null;
} else { } else {
Set<Material> parsed = MaterialHelper.loadMaterialList(new ArrayList<>(killPlaneBlockNames), "+", Set<Material> parsed = MaterialHelper.loadMaterialList(new ArrayList<>(killPlaneBlockNames), "+",
@ -402,9 +442,32 @@ public class ParkourArena implements Arena {
if (parsed.isEmpty()) { if (parsed.isEmpty()) {
return false; return false;
} }
this.killPlaneBlockNames = killPlaneBlockNames;
this.killPlaneBlocks = parsed; this.killPlaneBlocks = parsed;
} }
this.parkourArenaHandler.saveArenas(); this.saveArena();
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; return true;
} }
@ -420,7 +483,7 @@ public class ParkourArena implements Arena {
} }
this.checkpoints.add(checkpoint.clone()); this.checkpoints.add(checkpoint.clone());
this.parkourArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
@ -435,10 +498,23 @@ public class ParkourArena implements Arena {
} }
this.checkpoints.clear(); this.checkpoints.clear();
this.parkourArenaHandler.saveArenas(); this.saveArena();
return true; return true;
} }
/**
* Saves this arena to disk
*/
public void saveArena() {
try {
ParkourArenaStorageHelper.saveSingleParkourArena(this);
} catch (IOException exception) {
MiniGames.log(Level.SEVERE, "Unable to save arena! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, exception.getMessage());
}
}
@Override @Override
public boolean equals(Object other) { public boolean equals(Object other) {
if (!(other instanceof ParkourArena otherArena)) { if (!(other instanceof ParkourArena otherArena)) {

View File

@ -62,6 +62,13 @@ public enum ParkourArenaEditableProperty {
*/ */
KILL_PLANE_BLOCKS("killPlaneBlocks", (arena) -> String.valueOf(arena.getKillPlaneBlockNames()), KILL_PLANE_BLOCKS("killPlaneBlocks", (arena) -> String.valueOf(arena.getKillPlaneBlockNames()),
EditablePropertyType.MATERIAL_LIST), EditablePropertyType.MATERIAL_LIST),
/**
* The blocks used as this arena's obstacle blocks
*/
OBSTACLE_BLOCKS("obstacleBlocks", (arena) -> String.valueOf(arena.getObstacleBlockNames()),
EditablePropertyType.MATERIAL_LIST),
; ;
private final @NotNull String argumentString; private final @NotNull String argumentString;

View File

@ -42,6 +42,11 @@ public enum ParkourArenaStorageKey {
*/ */
KILL_PLANE_BLOCKS("killPlaneBlocks"), KILL_PLANE_BLOCKS("killPlaneBlocks"),
/**
* The key for this arena's obstacle blocks (overrides the config)
*/
OBSTACLE_BLOCKS("obstacleBlocks"),
/** /**
* The key for this arena's checkpoint locations * The key for this arena's checkpoint locations
*/ */

View File

@ -39,7 +39,6 @@ public class CreateDropperArenaCommand implements CommandExecutor {
} }
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler(); DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
DropperArena existingArena = arenaHandler.getArena(arenaName); DropperArena existingArena = arenaHandler.getArena(arenaName);
if (existingArena != null) { if (existingArena != null) {
MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender, MiniGames.getInstance().getStringFormatter().displayErrorMessage(commandSender,

View File

@ -40,7 +40,6 @@ public class CreateParkourArenaCommand implements CommandExecutor {
} }
ParkourArenaHandler arenaHandler = MiniGames.getInstance().getParkourArenaHandler(); ParkourArenaHandler arenaHandler = MiniGames.getInstance().getParkourArenaHandler();
ParkourArena existingArena = arenaHandler.getArena(arenaName); ParkourArena existingArena = arenaHandler.getArena(arenaName);
if (existingArena != null) { if (existingArena != null) {
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NAME_COLLISION); stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ARENA_NAME_COLLISION);

View File

@ -60,7 +60,12 @@ public class EditParkourArenaCommand implements CommandExecutor {
new String[]{editableProperty.getArgumentString(), value})); new String[]{editableProperty.getArgumentString(), value}));
return true; return true;
} else { } else {
boolean successful = changeValue(specifiedArena, editableProperty, arguments[2], player); boolean successful;
try {
successful = changeValue(specifiedArena, editableProperty, arguments[2], player);
} catch (NumberFormatException exception) {
successful = false;
}
if (successful) { if (successful) {
stringFormatter.displaySuccessMessage(player, stringFormatter.replacePlaceholder( stringFormatter.displaySuccessMessage(player, stringFormatter.replacePlaceholder(
MiniGameMessage.SUCCESS_PROPERTY_CHANGED, "{property}", MiniGameMessage.SUCCESS_PROPERTY_CHANGED, "{property}",
@ -80,9 +85,10 @@ public class EditParkourArenaCommand implements CommandExecutor {
* @param value <p>The new value of the property</p> * @param value <p>The new value of the property</p>
* @param player <p>The player trying to change the value</p> * @param player <p>The player trying to change the value</p>
* @return <p>True if the value was successfully changed</p> * @return <p>True if the value was successfully changed</p>
* @throws NumberFormatException <p>If unable to parse a given numeric value</p>
*/ */
private boolean changeValue(@NotNull ParkourArena arena, @NotNull ParkourArenaEditableProperty property, private boolean changeValue(@NotNull ParkourArena arena, @NotNull ParkourArenaEditableProperty property,
@NotNull String value, @NotNull Player player) { @NotNull String value, @NotNull Player player) throws NumberFormatException {
return switch (property) { return switch (property) {
case WIN_BLOCK_TYPE -> arena.setWinBlockType(parseMaterial(value)); case WIN_BLOCK_TYPE -> arena.setWinBlockType(parseMaterial(value));
case SPAWN_LOCATION -> arena.setSpawnLocation(parseLocation(player, value)); case SPAWN_LOCATION -> arena.setSpawnLocation(parseLocation(player, value));
@ -92,6 +98,7 @@ public class EditParkourArenaCommand implements CommandExecutor {
case CHECKPOINT_ADD -> arena.addCheckpoint(parseLocation(player, value)); case CHECKPOINT_ADD -> arena.addCheckpoint(parseLocation(player, value));
case CHECKPOINT_CLEAR -> arena.clearCheckpoints(); case CHECKPOINT_CLEAR -> arena.clearCheckpoints();
case KILL_PLANE_BLOCKS -> arena.setKillPlaneBlocks(new HashSet<>(List.of(value.split(",")))); case KILL_PLANE_BLOCKS -> arena.setKillPlaneBlocks(new HashSet<>(List.of(value.split(","))));
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 mustDoGroupedInSequence;
private boolean ignoreRecordsUntilGroupBeatenOnce; private boolean ignoreRecordsUntilGroupBeatenOnce;
private Set<Material> killPlaneBlocks; private Set<Material> killPlaneBlocks;
private Set<Material> obstacleBlocks;
/** /**
* Instantiates a new dropper configuration * Instantiates a new dropper configuration
@ -63,12 +64,22 @@ public class ParkourConfiguration extends MiniGameConfiguration {
return new HashSet<>(this.killPlaneBlocks); 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 @Override
protected void load() { protected void load() {
this.enforceCheckpointOrder = configuration.getBoolean(rootNode + "enforceCheckpointOrder", false); this.enforceCheckpointOrder = configuration.getBoolean(rootNode + "enforceCheckpointOrder", false);
this.mustDoGroupedInSequence = configuration.getBoolean(rootNode + "mustDoGroupedInSequence", true); this.mustDoGroupedInSequence = configuration.getBoolean(rootNode + "mustDoGroupedInSequence", true);
this.ignoreRecordsUntilGroupBeatenOnce = configuration.getBoolean(rootNode + "ignoreRecordsUntilGroupBeatenOnce", false); this.ignoreRecordsUntilGroupBeatenOnce = configuration.getBoolean(rootNode + "ignoreRecordsUntilGroupBeatenOnce", false);
this.killPlaneBlocks = loadMaterialList(rootNode + "killPlaneBlocks"); this.killPlaneBlocks = loadMaterialList(rootNode + "killPlaneBlocks");
this.obstacleBlocks = loadMaterialList(rootNode + "obstacleBlocks");
} }
@Override @Override
@ -82,6 +93,10 @@ public class ParkourConfiguration extends MiniGameConfiguration {
for (Material material : killPlaneBlocks) { for (Material material : killPlaneBlocks) {
builder.append("\n - ").append(material.name()); builder.append("\n - ").append(material.name());
} }
builder.append("\n" + "Obstacle blocks: ");
for (Material material : obstacleBlocks) {
builder.append("\n - ").append(material.name());
}
return builder.toString(); return builder.toString();
} }

View File

@ -3,6 +3,7 @@ package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames; import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena; import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.ArenaSession; import net.knarcraft.minigames.arena.ArenaSession;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode; import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession; import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import net.knarcraft.minigames.arena.parkour.ParkourArena; import net.knarcraft.minigames.arena.parkour.ParkourArena;
@ -14,11 +15,13 @@ import net.knarcraft.minigames.config.ParkourConfiguration;
import net.knarcraft.minigames.config.SharedConfiguration; import net.knarcraft.minigames.config.SharedConfiguration;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector; import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -32,6 +35,8 @@ import java.util.Set;
*/ */
public class MoveListener implements Listener { public class MoveListener implements Listener {
private static final BoundingBox fullBlockBox = new BoundingBox(0, 0, 0, 1, 1, 1);
private final DropperConfiguration dropperConfiguration; private final DropperConfiguration dropperConfiguration;
private final ParkourConfiguration parkourConfiguration; private final ParkourConfiguration parkourConfiguration;
@ -49,12 +54,15 @@ public class MoveListener implements Listener {
@EventHandler @EventHandler
public void onPlayerMove(PlayerMoveEvent event) { public void onPlayerMove(PlayerMoveEvent event) {
// Ignore if no actual movement is happening // Ignore if no actual movement is happening
if (event.getFrom().equals(event.getTo()) || event.getTo() == null) { if (event.getTo() == null) {
return; return;
} }
ArenaSession session = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId()); ArenaSession session = MiniGames.getInstance().getSession(event.getPlayer().getUniqueId());
if (session instanceof DropperArenaSession dropperSession) { if (session instanceof DropperArenaSession dropperSession) {
if (event.getFrom().equals(event.getTo())) {
return;
}
doDropperArenaChecks(event, dropperSession); doDropperArenaChecks(event, dropperSession);
} else if (session instanceof ParkourArenaSession parkourSession) { } else if (session instanceof ParkourArenaSession parkourSession) {
doParkourArenaChecks(event, parkourSession); doParkourArenaChecks(event, parkourSession);
@ -68,8 +76,7 @@ public class MoveListener implements Listener {
* @param arenaSession <p>The dropper session of the player triggering the event</p> * @param arenaSession <p>The dropper session of the player triggering the event</p>
*/ */
private void doParkourArenaChecks(@NotNull PlayerMoveEvent event, ParkourArenaSession arenaSession) { private void doParkourArenaChecks(@NotNull PlayerMoveEvent event, ParkourArenaSession arenaSession) {
// Ignore movement which won't cause the player's block to change if (event.getTo() == null) {
if (event.getTo() == null || event.getFrom().getBlock() == event.getTo().getBlock()) {
return; return;
} }
@ -189,30 +196,140 @@ public class MoveListener implements Listener {
* @param toLocation <p>The location the player's session is about to hit</p> * @param toLocation <p>The location the player's session is about to hit</p>
* @return <p>True if a special block has been hit</p> * @return <p>True if a special block has been hit</p>
*/ */
private boolean checkForSpecialBlock(ArenaSession arenaSession, Location toLocation) { private boolean checkForSpecialBlock(@NotNull ArenaSession arenaSession, @NotNull Location toLocation) {
SharedConfiguration sharedConfiguration = MiniGames.getInstance().getSharedConfiguration(); SharedConfiguration sharedConfiguration = MiniGames.getInstance().getSharedConfiguration();
double liquidDepth = sharedConfiguration.getLiquidHitBoxDepth();
double solidDepth = sharedConfiguration.getSolidHitBoxDistance(); double solidDepth = sharedConfiguration.getSolidHitBoxDistance();
double liquidDepth = sharedConfiguration.getLiquidHitBoxDepth();
Arena arena = arenaSession.getArena(); Arena arena = arenaSession.getArena();
// For water, only trigger when the player enters the water, but trigger earlier for everything else // For water, only trigger when the player enters the water, but trigger earlier for everything else
double depth = arena.winLocationIsSolid() ? solidDepth : liquidDepth; Set<Block> potentialWinTriggerBlocks;
for (Block block : getBlocksBeneathLocation(toLocation, depth)) { if (arena.winLocationIsSolid()) {
potentialWinTriggerBlocks = getBlocksBeneathLocation(toLocation, solidDepth);
} else {
potentialWinTriggerBlocks = getBlocksBeneathLocation(toLocation, liquidDepth);
}
for (Block block : potentialWinTriggerBlocks) {
if (arena.willCauseWin(block)) { if (arena.willCauseWin(block)) {
arenaSession.triggerWin(); arenaSession.triggerWin();
return true; return true;
} }
} }
// Check if the player is about to hit a non-air and non-liquid block if (arena instanceof DropperArena) {
for (Block block : getBlocksBeneathLocation(toLocation, solidDepth)) { // Check if the player is about to hit a non-air and non-liquid block
if (!block.getType().isAir() && arena.willCauseLoss(block)) { for (Block block : getBlocksBeneathLocation(toLocation, solidDepth)) {
if (!block.getType().isAir() && !block.isLiquid() && arena.willCauseLoss(block)) {
arenaSession.triggerLoss();
return true;
}
}
// Check if the player has entered a liquid that causes a loss
for (Block block : getBlocksBeneathLocation(toLocation, liquidDepth)) {
if (block.isLiquid() && arena.willCauseLoss(block)) {
arenaSession.triggerLoss();
return true;
}
}
} else if (arena instanceof ParkourArena) {
return checkParkourDeathBlock((ParkourArenaSession) arenaSession, toLocation);
}
return false;
}
/**
* Checks if a player is moving onto a block part of the parkour death plane
*
* @param arenaSession <p>The player's arena session</p>
* @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 ParkourArenaSession arenaSession,
@NotNull Location toLocation) {
// 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 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);
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()) {
// A collision shape's bounding box is relative to 0,0 and therefore must be adjusted to the block's
// location. Then overlap is checked by the player's collision box and the shifted bounding box.
if (playerBox.overlaps(boundingBox.clone().shift(block.getLocation()))) {
arenaSession.triggerLoss();
return true;
}
}
}
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(); arenaSession.triggerLoss();
return true; return true;
} }
} }
return false; return false;
} }
@ -223,12 +340,31 @@ public class MoveListener implements Listener {
* @return <p>The blocks beneath the player</p> * @return <p>The blocks beneath the player</p>
*/ */
private Set<Block> getBlocksBeneathLocation(Location location, double depth) { 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<>(); 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());
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; return blocksBeneath;
} }

View File

@ -97,22 +97,46 @@ public final class DropperArenaStorageHelper {
YamlConfiguration configuration = new YamlConfiguration(); YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(dropperArenasConfigurationSection); ConfigurationSection arenaSection = configuration.createSection(dropperArenasConfigurationSection);
for (DropperArena arena : arenas.values()) { for (DropperArena arena : arenas.values()) {
//Note: While the arena name is used as the key, as the key has to be sanitized, the un-sanitized arena name saveDropperArena(arenaSection, arena);
// must be stored as well
@NotNull ConfigurationSection configSection = arenaSection.createSection(arena.getArenaId().toString());
configSection.set(DropperArenaStorageKey.ID.getKey(), new SerializableUUID(arena.getArenaId()));
configSection.set(DropperArenaStorageKey.NAME.getKey(), arena.getArenaName());
configSection.set(DropperArenaStorageKey.SPAWN_LOCATION.getKey(), arena.getSpawnLocation());
configSection.set(DropperArenaStorageKey.EXIT_LOCATION.getKey(), arena.getExitLocation());
configSection.set(DropperArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey(), arena.getPlayerVerticalVelocity());
configSection.set(DropperArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey(), arena.getPlayerHorizontalVelocity());
configSection.set(DropperArenaStorageKey.WIN_BLOCK_TYPE.getKey(), new SerializableMaterial(arena.getWinBlockType()));
RewardStorageHelper.saveRewards(arena, configSection, DropperArenaStorageKey.REWARDS.getKey());
saveDropperArenaData(arena.getData());
} }
configuration.save(dropperArenaFile); configuration.save(dropperArenaFile);
} }
/**
* Saves a single arena
*
* @param arena <p>The arena to save</p>
* @throws IOException <p>If unable to write to the file</p>
*/
public static void saveSingleDropperArena(DropperArena arena) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(dropperArenasConfigurationSection);
saveDropperArena(arenaSection, arena);
configuration.save(dropperArenaFile);
}
/**
* Updates the given configuration section with the arena's data, and stores arena data for the arena
*
* @param arenaSection <p>The configuration section to update</p>
* @param arena <p>The arena to save</p>
* @throws IOException <p>If unable to save the arena data</p>
*/
private static void saveDropperArena(ConfigurationSection arenaSection, DropperArena arena) throws IOException {
//Note: While the arena name is used as the key, as the key has to be sanitized, the un-sanitized arena name
// must be stored as well
@NotNull ConfigurationSection configSection = arenaSection.createSection(arena.getArenaId().toString());
configSection.set(DropperArenaStorageKey.ID.getKey(), new SerializableUUID(arena.getArenaId()));
configSection.set(DropperArenaStorageKey.NAME.getKey(), arena.getArenaName());
configSection.set(DropperArenaStorageKey.SPAWN_LOCATION.getKey(), arena.getSpawnLocation());
configSection.set(DropperArenaStorageKey.EXIT_LOCATION.getKey(), arena.getExitLocation());
configSection.set(DropperArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey(), arena.getPlayerVerticalVelocity());
configSection.set(DropperArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey(), arena.getPlayerHorizontalVelocity());
configSection.set(DropperArenaStorageKey.WIN_BLOCK_TYPE.getKey(), new SerializableMaterial(arena.getWinBlockType()));
RewardStorageHelper.saveRewards(arena, configSection, DropperArenaStorageKey.REWARDS.getKey());
saveDropperArenaData(arena.getData());
}
/** /**
* Loads all arenas * Loads all arenas
* *

View File

@ -99,23 +99,76 @@ public final class ParkourArenaStorageHelper {
YamlConfiguration configuration = new YamlConfiguration(); YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(parkourArenasConfigurationSection); ConfigurationSection arenaSection = configuration.createSection(parkourArenasConfigurationSection);
for (ParkourArena arena : arenas.values()) { for (ParkourArena arena : arenas.values()) {
//Note: While the arena name is used as the key, as the key has to be sanitized, the un-sanitized arena name saveParkourArena(arenaSection, arena);
// must be stored as well
@NotNull ConfigurationSection configSection = arenaSection.createSection(arena.getArenaId().toString());
configSection.set(ParkourArenaStorageKey.ID.getKey(), new SerializableUUID(arena.getArenaId()));
configSection.set(ParkourArenaStorageKey.NAME.getKey(), arena.getArenaName());
configSection.set(ParkourArenaStorageKey.SPAWN_LOCATION.getKey(), arena.getSpawnLocation());
configSection.set(ParkourArenaStorageKey.EXIT_LOCATION.getKey(), arena.getExitLocation());
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(), arena.getKillPlaneBlockNames());
configSection.set(ParkourArenaStorageKey.CHECKPOINTS.getKey(), arena.getCheckpoints());
RewardStorageHelper.saveRewards(arena, configSection, ParkourArenaStorageKey.REWARDS.getKey());
saveParkourArenaData(arena.getData());
} }
configuration.save(parkourArenaFile); configuration.save(parkourArenaFile);
} }
/**
* Saves a single arena
*
* @param arena <p>The arena to save</p>
* @throws IOException <p>If unable to write to the file</p>
*/
public static void saveSingleParkourArena(ParkourArena arena) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(parkourArenasConfigurationSection);
saveParkourArena(arenaSection, arena);
configuration.save(parkourArenaFile);
}
/**
* Updates the given configuration section with the arena's data, and stores arena data for the arena
*
* @param arenaSection <p>The configuration section to update</p>
* @param arena <p>The arena to save</p>
* @throws IOException <p>If unable to save the arena data</p>
*/
private static void saveParkourArena(ConfigurationSection arenaSection, ParkourArena arena) throws IOException {
//Note: While the arena name is used as the key, as the key has to be sanitized, the un-sanitized arena name
// must be stored as well
@NotNull ConfigurationSection configSection = arenaSection.createSection(arena.getArenaId().toString());
configSection.set(ParkourArenaStorageKey.ID.getKey(), new SerializableUUID(arena.getArenaId()));
configSection.set(ParkourArenaStorageKey.NAME.getKey(), arena.getArenaName());
configSection.set(ParkourArenaStorageKey.SPAWN_LOCATION.getKey(), arena.getSpawnLocation());
configSection.set(ParkourArenaStorageKey.EXIT_LOCATION.getKey(), arena.getExitLocation());
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.OBSTACLE_BLOCKS.getKey(), getObstacleBlocks(arena));
configSection.set(ParkourArenaStorageKey.CHECKPOINTS.getKey(), arena.getCheckpoints());
RewardStorageHelper.saveRewards(arena, configSection, ParkourArenaStorageKey.REWARDS.getKey());
saveParkourArenaData(arena.getData());
}
/**
* Gets a list of the kill plane blocks for the given arena
*
* @param arena <p>The arena to get kill plane blocks for</p>
* @return <p>The kill plane blocks</p>
*/
private static List<String> getKillPlaneBlocks(ParkourArena arena) {
if (arena.getKillPlaneBlockNames() == null) {
return new ArrayList<>();
} else {
return new ArrayList<>(arena.getKillPlaneBlockNames());
}
}
/**
* 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 * Loads all arenas
* *
@ -163,7 +216,20 @@ public final class ParkourArenaStorageHelper {
Location winLocation = (Location) configurationSection.get(ParkourArenaStorageKey.WIN_LOCATION.getKey()); Location winLocation = (Location) configurationSection.get(ParkourArenaStorageKey.WIN_LOCATION.getKey());
SerializableMaterial winBlockType = (SerializableMaterial) configurationSection.get( SerializableMaterial winBlockType = (SerializableMaterial) configurationSection.get(
ParkourArenaStorageKey.WIN_BLOCK_TYPE.getKey()); ParkourArenaStorageKey.WIN_BLOCK_TYPE.getKey());
List<?> killPlaneBlockNames = configurationSection.getList(ParkourArenaStorageKey.KILL_PLANE_BLOCKS.getKey()); List<?> killPlaneBlockNamesList = configurationSection.getList(ParkourArenaStorageKey.KILL_PLANE_BLOCKS.getKey());
Set<String> killPlaneBlockNames;
if (killPlaneBlockNamesList == null) {
killPlaneBlockNames = null;
} else {
killPlaneBlockNames = new HashSet<>((List<String>) killPlaneBlockNamesList);
}
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()); List<Location> checkpoints = (List<Location>) configurationSection.get(ParkourArenaStorageKey.CHECKPOINTS.getKey());
Map<RewardCondition, Set<Reward>> rewards = RewardStorageHelper.loadRewards(configurationSection, Map<RewardCondition, Set<Reward>> rewards = RewardStorageHelper.loadRewards(configurationSection,
@ -195,7 +261,7 @@ public final class ParkourArenaStorageHelper {
} }
return new ParkourArena(arenaId, arenaName, spawnLocation, exitLocation, winBlockType.getRawValue(), winLocation, return new ParkourArena(arenaId, arenaName, spawnLocation, exitLocation, winBlockType.getRawValue(), winLocation,
(Set<String>) killPlaneBlockNames, checkpoints, rewards, arenaData, killPlaneBlockNames, obstacleBlockNames, checkpoints, rewards, arenaData,
MiniGames.getInstance().getParkourArenaHandler()); MiniGames.getInstance().getParkourArenaHandler());
} }

View File

@ -104,11 +104,30 @@ public final class TabCompleteHelper {
tabCompleteSuggestions.put(EditablePropertyType.BLOCK_TYPE, getBlockTypeSuggestions()); tabCompleteSuggestions.put(EditablePropertyType.BLOCK_TYPE, getBlockTypeSuggestions());
tabCompleteSuggestions.put(EditablePropertyType.CHECKPOINT_CLEAR, getCheckpointClearSuggestions()); tabCompleteSuggestions.put(EditablePropertyType.CHECKPOINT_CLEAR, getCheckpointClearSuggestions());
tabCompleteSuggestions.put(EditablePropertyType.MATERIAL_LIST, getMaterialListSuggestions()); tabCompleteSuggestions.put(EditablePropertyType.MATERIAL_LIST, getMaterialListSuggestions());
tabCompleteSuggestions.put(EditablePropertyType.DOUBLE, getDoubleSuggestions());
} }
return tabCompleteSuggestions.get(propertyType); return tabCompleteSuggestions.get(propertyType);
} }
/**
* Gets suggestions for double values
*
* @return <p>A list of suggestions</p>
*/
private static List<String> getDoubleSuggestions() {
List<String> suggestions = new ArrayList<>();
suggestions.add("0");
suggestions.add("0.01");
suggestions.add("0.1");
suggestions.add("0.2");
suggestions.add("0.3");
suggestions.add("0.4");
suggestions.add("0.5");
suggestions.add("1");
return suggestions;
}
/** /**
* Gets suggestions for a list of materials * Gets suggestions for a list of materials
* *
@ -118,6 +137,7 @@ public final class TabCompleteHelper {
List<String> suggestions = new ArrayList<>(); List<String> suggestions = new ArrayList<>();
suggestions.add("LAVA,MAGMA_BLOCK"); suggestions.add("LAVA,MAGMA_BLOCK");
suggestions.add("WATER,MAGMA_BLOCK,LAVA,+BUTTONS,+CORALS"); suggestions.add("WATER,MAGMA_BLOCK,LAVA,+BUTTONS,+CORALS");
suggestions.add("CHAIN,END_ROD,LIGHTNING_ROD");
return suggestions; return suggestions;
} }

View File

@ -20,6 +20,12 @@ parkour:
killPlaneBlocks: killPlaneBlocks:
- LAVA - LAVA
- MAGMA_BLOCK - 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: dropper:
# Whether to block using the shift key to drop faster than the intended drop speed # Whether to block using the shift key to drop faster than the intended drop speed
blockSneaking: true blockSneaking: true
@ -63,6 +69,7 @@ dropper:
- +BUTTONS - +BUTTONS
- +CORALS - +CORALS
- +WALL_CORALS - +WALL_CORALS
- LIGHT
shared: shared:
# This decides how far inside a non-solid block the player must go before detection triggers (-1, 0). The closer to -1 # This decides how far inside a non-solid block the player must go before detection triggers (-1, 0). The closer to -1
# it is, the more accurate it will seem to the player, but the likelihood of not detecting the hit increases. # it is, the more accurate it will seem to the player, but the likelihood of not detecting the hit increases.

View File

@ -82,15 +82,15 @@ commands:
aliases: aliases:
- dcreate - dcreate
permission: minigames.create.dropper permission: minigames.create.dropper
usage: | usage: /<command> <arena>
/<command> <arena> <property> [new value]
- Valid properties: name, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity, winBlockType
description: Used to create a new dropper arena description: Used to create a new dropper arena
dropperEdit: dropperEdit:
aliases: aliases:
- dedit - dedit
permission: minigames.edit.dropper permission: minigames.edit.dropper
usage: /<command> (Details not finalized) usage: |
/<command> <arena> <property> [new value]
- Valid properties: name, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity, winBlockType
description: Used to edit an existing dropper arena description: Used to edit an existing dropper arena
dropperRemove: dropperRemove:
aliases: aliases:
@ -143,19 +143,19 @@ commands:
aliases: aliases:
- pcreate - pcreate
permission: minigames.create.parkour permission: minigames.create.parkour
usage: | usage: /<command> <arena>
/<command> <arena> <property> [new value]
- Valid properties: name, spawnLocation, exitLocation, winBlockType, winLocation, checkpointAdd, checkpointClear, killPlaneBlocks
description: Used to create a new parkour arena description: Used to create a new parkour arena
parkourEdit: parkourEdit:
aliases: aliases:
- pedit - pedit
permission: minigames.edit.parkour permission: minigames.edit.parkour
usage: /<command> (Details not finalized) usage: |
/<command> <arena> <property> [new value]
- Valid properties: name, spawnLocation, exitLocation, winBlockType, winLocation, checkpointAdd, checkpointClear, killPlaneBlocks
description: Used to edit an existing parkour arena description: Used to edit an existing parkour arena
parkourRemove: parkourRemove:
aliases: aliases:
- dremove - premove
permission: minigames.remove.parkour permission: minigames.remove.parkour
usage: /<command> <arena> usage: /<command> <arena>
description: Used to remove an existing parkour arena description: Used to remove an existing parkour arena