Compare commits

...

21 Commits
v1.0 ... master

Author SHA1 Message Date
d7e950c53e Improves display of time records 2024-04-04 15:50:38 +02:00
c8fbdec64f Fixes some minor issues
Fixes the tab-completion for the randomly inverted game-mode not being properly matched.
Prevents the usage text from displaying for some join errors
2024-04-04 04:28:44 +02:00
09f29e5f67
Merge pull request #30 from SunNetservers/dev
Arena saving bugfix
2024-04-04 03:07:30 +02:00
7213bd0c70 Fixes README formatting 2024-04-04 03:03:57 +02:00
a23d0c769f Bumps version 2024-04-04 03:01:28 +02:00
142cd5339b Fixes a bug in arena saving
Saving a single arena caused all other arena data of that type to be deleted.
2024-04-04 03:00:03 +02:00
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
27 changed files with 538 additions and 169 deletions

View File

@ -99,7 +99,9 @@ This reward requires an argument which is the permission string you want to gran
###### Command
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
@ -211,16 +213,17 @@ 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. |
| 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
### Shared
| Name | Type | Default | Description |
|-----------------------------------|---------------------|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| liquidHitBoxDepth | -1 < decimal < 0 | -0.8 | 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. |
| solidHitBoxDistance | 0 < decimal < 1 | 0.2 | This decides the distance the player must be from a block below them before a hit triggers (0, 1). If too low, the likelihood of detecting the hit decreases, but it won't look like the player hit the block without being near. |
| Name | Type | Default | Description |
|---------------------|------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| liquidHitBoxDepth | -1 < decimal < 0 | -0.8 | 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. |
| solidHitBoxDistance | 0 < decimal < 1 | 0.2 | This decides the distance the player must be from a block below them before a hit triggers (0, 1). If too low, the likelihood of detecting the hit decreases, but it won't look like the player hit the block without being near. |
### Dropper
@ -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 |
| 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:
@ -267,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

@ -6,7 +6,7 @@
<groupId>net.knarcraft</groupId>
<artifactId>MiniGames</artifactId>
<version>1.0</version>
<version>1.2</version>
<packaging>jar</packaging>
<name>MiniGames</name>
@ -105,12 +105,6 @@
<version>24.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>

View File

@ -104,7 +104,7 @@ public abstract class AbstractArenaPlayerRegistry<K extends Arena> implements Ar
for (PlayerEntryState entryState : entryStates) {
this.entryStates.put(entryState.getPlayerId(), entryState);
}
if (this.entryStates.size() > 0) {
if (!this.entryStates.isEmpty()) {
MiniGames.log(Level.WARNING, entryStates.size() + " un-exited sessions found. This happens if " +
"players leave in the middle of a game, or if the server crashes. MiniGames will do its best " +
"to fix the players' states.");

View File

@ -38,6 +38,11 @@ public enum EditablePropertyType {
/**
* 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.Set;
import java.util.UUID;
import java.util.logging.Level;
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) {
this.rewards.computeIfAbsent(rewardCondition, k -> new HashSet<>());
this.rewards.get(rewardCondition).add(reward);
this.dropperArenaHandler.saveArenas();
this.saveArena();
}
@Override
public void clearRewards(@NotNull RewardCondition rewardCondition) {
this.rewards.remove(rewardCondition);
this.dropperArenaHandler.saveArenas();
this.saveArena();
}
@Override
@ -267,7 +268,7 @@ public class DropperArena implements Arena {
return false;
} else {
this.spawnLocation = newLocation;
this.dropperArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -283,7 +284,7 @@ public class DropperArena implements Arena {
return false;
} else {
this.exitLocation = newLocation;
this.dropperArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -300,7 +301,7 @@ public class DropperArena implements Arena {
this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately
this.dropperArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
this.dropperArenaHandler.saveArenas();
this.saveArena();
return true;
} else {
return false;
@ -320,7 +321,7 @@ public class DropperArena implements Arena {
return false;
} else {
this.winBlockType = material;
this.dropperArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -338,7 +339,7 @@ public class DropperArena implements Arena {
return false;
} else {
this.playerHorizontalVelocity = horizontalVelocity;
this.dropperArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -354,11 +355,24 @@ public class DropperArena implements Arena {
return false;
} else {
this.playerVerticalVelocity = verticalVelocity;
this.dropperArenaHandler.saveArenas();
this.saveArena();
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
public boolean equals(Object other) {
if (!(other instanceof DropperArena otherArena)) {

View File

@ -34,11 +34,11 @@ public enum DropperArenaGameMode implements ConfigurationSerializable, ArenaGame
* @param gameMode <p>The game-mode string to match</p>
* @return <p>The specified arena game-mode</p>
*/
public static @NotNull DropperArenaGameMode matchGamemode(@NotNull String gameMode) {
public static @NotNull DropperArenaGameMode matchGameMode(@NotNull String gameMode) {
String sanitized = gameMode.trim().toLowerCase();
if (sanitized.matches("(invert(ed)?|inverse)")) {
return DropperArenaGameMode.INVERTED;
} else if (sanitized.matches("rand(om)?")) {
} else if (sanitized.matches("rand(om)?_?(inverted)?")) {
return DropperArenaGameMode.RANDOM_INVERTED;
} else {
return DropperArenaGameMode.DEFAULT;

View File

@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import static net.knarcraft.minigames.util.InputValidationHelper.isInvalid;
@ -72,6 +73,16 @@ public class ParkourArena implements Arena {
*/
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.
*/
@ -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 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 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>
@ -103,7 +115,8 @@ 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, @NotNull List<Location> checkpoints,
@Nullable Set<String> killPlaneBlockNames, @Nullable Set<String> obstacleBlockNames,
@NotNull List<Location> checkpoints,
@NotNull Map<RewardCondition, Set<Reward>> rewards,
@NotNull ParkourArenaData parkourArenaData, @NotNull ParkourArenaHandler arenaHandler) {
this.arenaId = arenaId;
@ -115,6 +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.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;
@ -147,6 +163,7 @@ 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;
}
@ -180,13 +197,13 @@ public class ParkourArena implements Arena {
public void addReward(@NotNull RewardCondition rewardCondition, @NotNull Reward reward) {
this.rewards.computeIfAbsent(rewardCondition, k -> new HashSet<>());
this.rewards.get(rewardCondition).add(reward);
this.parkourArenaHandler.saveArenas();
this.saveArena();
}
@Override
public void clearRewards(@NotNull RewardCondition rewardCondition) {
this.rewards.remove(rewardCondition);
this.parkourArenaHandler.saveArenas();
this.saveArena();
}
@Override
@ -240,6 +257,28 @@ public class ParkourArena implements Arena {
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
*
@ -294,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
@ -314,7 +353,7 @@ public class ParkourArena implements Arena {
return false;
} else {
this.spawnLocation = newLocation;
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -330,7 +369,7 @@ public class ParkourArena implements Arena {
return false;
} else {
this.exitLocation = newLocation;
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -347,7 +386,7 @@ public class ParkourArena implements Arena {
this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately
this.parkourArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
} else {
return false;
@ -367,7 +406,7 @@ public class ParkourArena implements Arena {
return false;
} else {
this.winBlockType = material;
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -383,7 +422,7 @@ public class ParkourArena implements Arena {
return false;
} else {
this.winLocation = newLocation.clone();
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
}
@ -395,6 +434,7 @@ public class ParkourArena implements Arena {
*/
public boolean setKillPlaneBlocks(@NotNull Set<String> killPlaneBlockNames) {
if (killPlaneBlockNames.isEmpty()) {
this.killPlaneBlockNames = null;
this.killPlaneBlocks = null;
} else {
Set<Material> parsed = MaterialHelper.loadMaterialList(new ArrayList<>(killPlaneBlockNames), "+",
@ -402,9 +442,32 @@ public class ParkourArena implements Arena {
if (parsed.isEmpty()) {
return false;
}
this.killPlaneBlockNames = killPlaneBlockNames;
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;
}
@ -420,7 +483,7 @@ public class ParkourArena implements Arena {
}
this.checkpoints.add(checkpoint.clone());
this.parkourArenaHandler.saveArenas();
this.saveArena();
return true;
}
@ -435,10 +498,23 @@ public class ParkourArena implements Arena {
}
this.checkpoints.clear();
this.parkourArenaHandler.saveArenas();
this.saveArena();
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
public boolean equals(Object other) {
if (!(other instanceof ParkourArena otherArena)) {

View File

@ -62,6 +62,13 @@ public enum ParkourArenaEditableProperty {
*/
KILL_PLANE_BLOCKS("killPlaneBlocks", (arena) -> String.valueOf(arena.getKillPlaneBlockNames()),
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;

View File

@ -42,6 +42,11 @@ public enum ParkourArenaStorageKey {
*/
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
*/

View File

@ -44,6 +44,13 @@ public abstract class ArenaRecord<K extends Comparable<K>> implements Comparable
return record;
}
/**
* Gets this as a string that should be printed on a sign
*
* @return <p>This as a string</p>
*/
public abstract String getAsString();
@Override
public boolean equals(Object other) {
return other instanceof ArenaRecord<?> && userId.equals(((ArenaRecord<?>) other).userId);
@ -70,7 +77,7 @@ public abstract class ArenaRecord<K extends Comparable<K>> implements Comparable
@Override
public String toString() {
return userId + ": " + record;
return userId + ":" + record;
}
}

View File

@ -19,6 +19,11 @@ public class IntegerRecord extends SummableArenaRecord<Integer> {
super(userId, record);
}
@Override
public String getAsString() {
return String.valueOf(this.getRecord());
}
@Override
public SummableArenaRecord<Integer> sum(Integer value) {
return new IntegerRecord(this.getUserId(), this.getRecord() + value);

View File

@ -7,7 +7,7 @@ import java.util.Map;
import java.util.UUID;
/**
* A record storing a Long
* A record storing a Long time
*/
public class LongRecord extends SummableArenaRecord<Long> {
@ -29,6 +29,22 @@ public class LongRecord extends SummableArenaRecord<Long> {
return new LongRecord(this.getUserId(), this.getRecord() + value);
}
@Override
public String getAsString() {
int seconds = (int) Math.floor(getRecord() / 1000.0);
int minutes = 0;
if (seconds > 60) {
minutes = (int) Math.floor(seconds / 60.0);
seconds = seconds % 60;
}
if (minutes > 0) {
return minutes + "m" + seconds + "s";
} else {
return seconds + "s";
}
}
/**
* Deserializes the saved arena record
*

View File

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

View File

@ -38,7 +38,7 @@ public class JoinDropperArenaCommand implements CommandExecutor {
// Disallow joining if the player is already in a mini-game arena
if (MiniGames.getInstance().getSession(player.getUniqueId()) != null) {
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ALREADY_PLAYING);
return false;
return true;
}
// Make sure the arena exists
@ -51,10 +51,11 @@ public class JoinDropperArenaCommand implements CommandExecutor {
// Deny vehicles as allowing this is tricky, and will cause problems in some cases
if (player.isInsideVehicle() || !player.getPassengers().isEmpty()) {
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_JOIN_IN_VEHICLE_OR_PASSENGER);
return false;
return true;
}
return joinArena(specifiedArena, player, arguments);
joinArena(specifiedArena, player, arguments);
return true;
}
/**
@ -63,13 +64,12 @@ public class JoinDropperArenaCommand implements CommandExecutor {
* @param specifiedArena <p>The arena the player wants to join</p>
* @param player <p>The player joining the arena</p>
* @param arguments <p>The arguments given</p>
* @return <p>Whether the arena was joined successfully</p>
*/
private boolean joinArena(DropperArena specifiedArena, Player player, String[] arguments) {
private void joinArena(DropperArena specifiedArena, Player player, String[] arguments) {
// Find the specified game-mode
DropperArenaGameMode gameMode;
if (arguments.length > 1) {
gameMode = DropperArenaGameMode.matchGamemode(arguments[1]);
gameMode = DropperArenaGameMode.matchGameMode(arguments[1]);
} else {
gameMode = DropperArenaGameMode.DEFAULT;
}
@ -77,7 +77,7 @@ public class JoinDropperArenaCommand implements CommandExecutor {
// Make sure the player has beaten the necessary levels
DropperArenaGroup arenaGroup = MiniGames.getInstance().getDropperArenaHandler().getGroup(specifiedArena.getArenaId());
if (arenaGroup != null && !doGroupChecks(specifiedArena, arenaGroup, gameMode, player)) {
return false;
return;
}
StringFormatter stringFormatter = MiniGames.getInstance().getStringFormatter();
@ -87,7 +87,7 @@ public class JoinDropperArenaCommand implements CommandExecutor {
gameMode != DropperArenaGameMode.DEFAULT &&
specifiedArena.getData().hasNotCompleted(DropperArenaGameMode.DEFAULT, player)) {
stringFormatter.displayErrorMessage(player, MiniGameMessage.ERROR_NORMAL_MODE_REQUIRED);
return false;
return;
}
// Register the player's session
@ -103,14 +103,12 @@ public class JoinDropperArenaCommand implements CommandExecutor {
if (!teleported) {
stringFormatter.displayErrorMessage(player, MiniGameMessage.ERROR_ARENA_TELEPORT_FAILED);
newSession.triggerQuit(false, true);
return false;
} else {
// Update the player's state to follow the arena's rules
newSession.getEntryState().setArenaState();
player.getInventory().addItem(GUIHelper.getGUIOpenItem());
stringFormatter.displaySuccessMessage(player, MiniGameMessage.SUCCESS_ARENA_JOINED);
return true;
}
}

View File

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

View File

@ -60,7 +60,12 @@ public class EditParkourArenaCommand implements CommandExecutor {
new String[]{editableProperty.getArgumentString(), value}));
return true;
} 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) {
stringFormatter.displaySuccessMessage(player, stringFormatter.replacePlaceholder(
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 player <p>The player trying to change the value</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,
@NotNull String value, @NotNull Player player) {
@NotNull String value, @NotNull Player player) throws NumberFormatException {
return switch (property) {
case WIN_BLOCK_TYPE -> arena.setWinBlockType(parseMaterial(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_CLEAR -> arena.clearCheckpoints();
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

@ -38,7 +38,7 @@ public class JoinParkourArenaCommand implements CommandExecutor {
// Disallow joining if the player is already in a mini-game arena
if (MiniGames.getInstance().getSession(player.getUniqueId()) != null) {
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_ALREADY_PLAYING);
return false;
return true;
}
// Make sure the arena exists
@ -51,10 +51,11 @@ public class JoinParkourArenaCommand implements CommandExecutor {
// Deny vehicles as allowing this is tricky, and will cause problems in some cases
if (player.isInsideVehicle() || !player.getPassengers().isEmpty()) {
stringFormatter.displayErrorMessage(commandSender, MiniGameMessage.ERROR_JOIN_IN_VEHICLE_OR_PASSENGER);
return false;
return true;
}
return joinArena(specifiedArena, player, arguments);
joinArena(specifiedArena, player, arguments);
return true;
}
/**
@ -63,9 +64,8 @@ public class JoinParkourArenaCommand implements CommandExecutor {
* @param specifiedArena <p>The arena the player wants to join</p>
* @param player <p>The player joining the arena</p>
* @param arguments <p>The arguments given</p>
* @return <p>Whether the arena was joined successfully</p>
*/
private boolean joinArena(ParkourArena specifiedArena, Player player, String[] arguments) {
private void joinArena(ParkourArena specifiedArena, Player player, String[] arguments) {
// Find the specified game-mode
ParkourArenaGameMode gameMode;
if (arguments.length > 1) {
@ -78,13 +78,13 @@ public class JoinParkourArenaCommand implements CommandExecutor {
if (specifiedArena.hasNoCheckpoints() && gameMode == ParkourArenaGameMode.HARDCORE) {
MiniGames.getInstance().getStringFormatter().displayErrorMessage(player,
MiniGameMessage.ERROR_HARDCORE_NO_CHECKPOINTS);
return false;
return;
}
// Make sure the player has beaten the necessary levels
ParkourArenaGroup arenaGroup = MiniGames.getInstance().getParkourArenaHandler().getGroup(specifiedArena.getArenaId());
if (arenaGroup != null && !doGroupChecks(specifiedArena, arenaGroup, gameMode, player)) {
return false;
return;
}
// Register the player's session
@ -101,7 +101,6 @@ public class JoinParkourArenaCommand implements CommandExecutor {
MiniGames.getInstance().getStringFormatter().displayErrorMessage(player,
MiniGameMessage.ERROR_ARENA_TELEPORT_FAILED);
newSession.triggerQuit(false, true);
return false;
} else {
// Update the player's state to follow the arena's rules
newSession.getEntryState().setArenaState();
@ -109,7 +108,6 @@ public class JoinParkourArenaCommand implements CommandExecutor {
player.getInventory().addItem(GUIHelper.getGUIOpenItem());
MiniGames.getInstance().getStringFormatter().displaySuccessMessage(player,
MiniGameMessage.SUCCESS_ARENA_JOINED);
return true;
}
}

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

@ -3,6 +3,7 @@ package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
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.DropperArenaSession;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
@ -14,11 +15,13 @@ 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;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
@ -32,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;
@ -49,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);
@ -68,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 || event.getFrom().getBlock() == event.getTo().getBlock()) {
if (event.getTo() == null) {
return;
}
@ -189,30 +196,140 @@ public class MoveListener implements Listener {
* @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>
*/
private boolean checkForSpecialBlock(ArenaSession arenaSession, Location toLocation) {
private boolean checkForSpecialBlock(@NotNull ArenaSession arenaSession, @NotNull Location toLocation) {
SharedConfiguration sharedConfiguration = MiniGames.getInstance().getSharedConfiguration();
double liquidDepth = sharedConfiguration.getLiquidHitBoxDepth();
double solidDepth = sharedConfiguration.getSolidHitBoxDistance();
double liquidDepth = sharedConfiguration.getLiquidHitBoxDepth();
Arena arena = arenaSession.getArena();
// For water, only trigger when the player enters the water, but trigger earlier for everything else
double depth = arena.winLocationIsSolid() ? solidDepth : liquidDepth;
for (Block block : getBlocksBeneathLocation(toLocation, depth)) {
Set<Block> potentialWinTriggerBlocks;
if (arena.winLocationIsSolid()) {
potentialWinTriggerBlocks = getBlocksBeneathLocation(toLocation, solidDepth);
} else {
potentialWinTriggerBlocks = getBlocksBeneathLocation(toLocation, liquidDepth);
}
for (Block block : potentialWinTriggerBlocks) {
if (arena.willCauseWin(block)) {
arenaSession.triggerWin();
return true;
}
}
// Check if the player is about to hit a non-air and non-liquid block
for (Block block : getBlocksBeneathLocation(toLocation, solidDepth)) {
if (!block.getType().isAir() && arena.willCauseLoss(block)) {
if (arena instanceof DropperArena) {
// Check if the player is about to hit a non-air and non-liquid 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();
return true;
}
}
return false;
}
@ -223,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

@ -26,7 +26,7 @@ public class DropperRecordExpansion extends RecordExpansion {
@Override
protected @NotNull ArenaGameMode parseGameMode(@NotNull String gameMode) {
return DropperArenaGameMode.matchGamemode(gameMode);
return DropperArenaGameMode.matchGameMode(gameMode);
}
}

View File

@ -313,8 +313,8 @@ public abstract class RecordExpansion extends PlaceholderExpansion {
private String getRecordData(@NotNull InfoType infoType, @NotNull ArenaRecord<?> arenaRecord) {
return switch (infoType) {
case PLAYER -> getPlayerName(arenaRecord.getUserId());
case VALUE -> arenaRecord.getRecord().toString();
case COMBINED -> getPlayerName(arenaRecord.getUserId()) + ": " + arenaRecord.getRecord().toString();
case VALUE -> arenaRecord.getAsString();
case COMBINED -> getPlayerName(arenaRecord.getUserId()) + ": " + arenaRecord.getAsString();
};
}

View File

@ -20,6 +20,7 @@ import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.yaml.snakeyaml.error.YAMLException;
import java.io.File;
import java.io.IOException;
@ -97,22 +98,49 @@ public final class DropperArenaStorageHelper {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(dropperArenasConfigurationSection);
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
// 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());
saveDropperArena(arenaSection, arena);
}
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 = YamlConfiguration.loadConfiguration(dropperArenaFile);
ConfigurationSection arenaSection = configuration.getConfigurationSection(dropperArenasConfigurationSection);
if (arenaSection == null) {
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
*
@ -222,9 +250,14 @@ public final class DropperArenaStorageHelper {
* @return <p>The loaded arena data</p>
*/
private static @Nullable DropperArenaData loadDropperArenaData(@NotNull UUID arenaId) {
File arenaDataFile = getDropperArenaDataFile(arenaId);
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(arenaDataFile);
return (DropperArenaData) configuration.get(DropperArenaStorageKey.DATA.getKey());
try {
File arenaDataFile = getDropperArenaDataFile(arenaId);
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(arenaDataFile);
return (DropperArenaData) configuration.get(DropperArenaStorageKey.DATA.getKey());
} catch (YAMLException exception) {
MiniGames.log(Level.SEVERE, "Unable to load arena data from arena " + arenaId);
return null;
}
}
/**

View File

@ -99,23 +99,79 @@ public final class ParkourArenaStorageHelper {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(parkourArenasConfigurationSection);
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
// 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());
saveParkourArena(arenaSection, arena);
}
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 = YamlConfiguration.loadConfiguration(parkourArenaFile);
ConfigurationSection arenaSection = configuration.getConfigurationSection(parkourArenasConfigurationSection);
if (arenaSection == null) {
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
*
@ -163,7 +219,20 @@ public final class ParkourArenaStorageHelper {
Location winLocation = (Location) configurationSection.get(ParkourArenaStorageKey.WIN_LOCATION.getKey());
SerializableMaterial winBlockType = (SerializableMaterial) configurationSection.get(
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());
Map<RewardCondition, Set<Reward>> rewards = RewardStorageHelper.loadRewards(configurationSection,
@ -195,7 +264,7 @@ public final class ParkourArenaStorageHelper {
}
return new ParkourArena(arenaId, arenaName, spawnLocation, exitLocation, winBlockType.getRawValue(), winLocation,
(Set<String>) killPlaneBlockNames, checkpoints, rewards, arenaData,
killPlaneBlockNames, obstacleBlockNames, checkpoints, rewards, arenaData,
MiniGames.getInstance().getParkourArenaHandler());
}

View File

@ -104,11 +104,30 @@ public final class TabCompleteHelper {
tabCompleteSuggestions.put(EditablePropertyType.BLOCK_TYPE, getBlockTypeSuggestions());
tabCompleteSuggestions.put(EditablePropertyType.CHECKPOINT_CLEAR, getCheckpointClearSuggestions());
tabCompleteSuggestions.put(EditablePropertyType.MATERIAL_LIST, getMaterialListSuggestions());
tabCompleteSuggestions.put(EditablePropertyType.DOUBLE, getDoubleSuggestions());
}
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
*
@ -118,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
@ -63,6 +69,7 @@ dropper:
- +BUTTONS
- +CORALS
- +WALL_CORALS
- LIGHT
shared:
# 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.

View File

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

View File

@ -1,51 +0,0 @@
package net.knarcraft.minigames.arena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Tests for arena dropper groups
*/
public class MiniGamesArenaGroupTest {
@Test
public void swapTest() {
/*
This test makes sure the order of arenas is as expected when the arenas are added to a group. It also makes
sure that swapping two items works as expected.
*/
DropperArenaGroup arenaGroup = new DropperArenaGroup("test");
UUID arena1Id = UUID.randomUUID();
UUID arena2Id = UUID.randomUUID();
UUID arena3Id = UUID.randomUUID();
UUID arena4Id = UUID.randomUUID();
arenaGroup.addArena(arena1Id);
arenaGroup.addArena(arena2Id);
arenaGroup.addArena(arena3Id);
arenaGroup.addArena(arena4Id);
List<UUID> initialOrder = new ArrayList<>();
initialOrder.add(arena1Id);
initialOrder.add(arena2Id);
initialOrder.add(arena3Id);
initialOrder.add(arena4Id);
Assertions.assertEquals(initialOrder, arenaGroup.getArenas());
arenaGroup.swapArenas(1, 3);
List<UUID> swapped = new ArrayList<>();
swapped.add(arena1Id);
swapped.add(arena4Id);
swapped.add(arena3Id);
swapped.add(arena2Id);
Assertions.assertEquals(swapped, arenaGroup.getArenas());
}
}