Merge pull request #17 from SunNetservers/dev

Many changes and implementations
This commit is contained in:
Kristian Knarvik 2023-03-31 16:48:39 +00:00 committed by GitHub
commit fb6550afe7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1689 additions and 285 deletions

104
README.md
View File

@ -8,47 +8,93 @@ To modify
## Permissions
| Node | Description |
|----------------|---------------------------------------------------|
| dropper.admin | Gives all permissions |
| dropper.join | Allows a player to participate in dropper arenas |
| dropper.create | Allows a player to create a new dropper arena |
| dropper.edit | Allows a player to edit an existing dropper arena |
| dropper.remove | Allows a player to remove a dropper arena |
| Node | Description |
|----------------|----------------------------------------------------|
| dropper.admin | Gives all permissions. |
| dropper.join | Allows a player to participate in dropper arenas. |
| dropper.create | Allows a player to create a new dropper arena. |
| dropper.edit | Allows a player to edit an existing dropper arena. |
| dropper.remove | Allows a player to remove a dropper arena. |
## Commands
| Command | Arguments | Description |
|------------------------------|-----------------------------|-------------------------------------------------|
| /dropperlist | | Lists available dropper arenas |
| [/dropperjoin](#dropperjoin) | \<arena> \[mode] | Joins the selected arena |
| /dropperleave | | Leaves the current dropper arena |
| /droppercreate | \<name> | Creates a new dropper arena with the given name |
| /dropperremove | \<arena> | Removes the specified dropper arena |
| [/dropperedit](#dropperedit) | \<arena> \<option> \[value] | Gets or sets a dropper arena option |
| /dropperreload | | Reloads all data from disk |
| Command | Alias | Arguments | Description |
|-----------------------------------------|----------|-----------------------------|-------------------------------------------------------------------------------------|
| /dropperList | /dlist | | Lists available dropper arenas. |
| [/dropperJoin](#/dropperJoin) | /djoin | \<arena> \[mode] | Joins the selected arena. |
| /dropperLeave | /dleave | | Leaves the current dropper arena. |
| /dropperCreate | /dcreate | \<name> | Creates a new dropper arena with the given name. The spawn is set to your location. |
| /dropperRemove | /dremove | \<arena> | Removes the specified dropper arena. |
| [/dropperEdit](#/dropperEdit) | /dedit | \<arena> \<option> \[value] | Gets or sets a dropper arena option. |
| /dropperReload | /dreload | | Reloads all data from disk. |
| [/dropperGroupSet](#/dropperGroupSet) | /dgset | \<arena> \<group> | Puts the given arena in the given group. Use "none" to remove an existing group. |
| /dropperGroupList | /dglist | \[group] | Lists groups, or the stages of a group if a group is specified. |
| [/dropperGroupSwap](#/dropperGroupSwap) | /dgswap | \<arena1> \<arena2> | Swaps the two arenas in the group's ordered list. |
## Command explanation
### /dropperjoin
### /dropperJoin
This command is used for joining a dropper arena.
`/droppejoin <arena> [mode]`
`/dropperjoin <arena> [mode]`
| Argument | Usage |
|----------|------------------------------------------------------------------------------------------------------------------|
| arena | The name of the arena to join |
| mode | Additional challenge modes can be played after an arena has been cleared once. Available modes: deaths and time. |
| Argument | Usage |
|----------|----------------------------------------------------------------------------------------------------------------------|
| arena | The name of the arena to join. |
| mode | Additional challenge modes can be played after an arena has been cleared once. Available modes: inverted and random. |
### /dropperedit
### /dropperEdit
This command allows editing the specified property for the specified dropper arena
This command allows editing the specified property for the specified dropper arena.
`/dropperedit <arena> <option> [value]`
| Argument | Usage |
|----------|--------------------------------------|
| arena | The name of the arena to edit |
| option | The option to display or change |
| value | The new value of the selected option |
| Argument | Usage |
|----------|---------------------------------------|
| arena | The name of the arena to edit. |
| option | The option to display or change. |
| value | The new value of the selected option. |
These are all the options that can be changed for an arena.
| Option | Details |
|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | The name of the arena. Used mainly to select the arena in commands. |
| 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. |
| verticalVelocity | The vertical velocity set for players in the arena (basically their falling speed). It must be greater than 0, but max 75. `12.565` and other decimals are allowed. |
| horizontalVelocity | The horizontal velocity (technically fly speed) set for players in the arena. It must be between 0 and 1, and cannot be 0. Decimals are allowed. |
| 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. |
### /dropperGroupSet
This command is used to set the group of an arena
`/droppergroupset <arena> <group>`
Dropper groups are created and removed as necessary. If you specify a group named "potato", that group is created, and
will be used again if you specify the "potato" group for another arena. You use "none" or "null" to remove an arena from
its group. If the group has no arenas, it will be automatically removed. If the arena already is in a group, it will be
moved to the new group.
### /dropperGroupSwap
This command is used for changing the order of arenas within a group.
`/droppergroupswap <arena1> <arena2>`
Groups define an order the arenas within that group has to be completed in. Use `/droppergrouplist group` to see the
actual order of the group. So, assuming your arenas in the group looked something like:
1. Forest
2. Sea
3. Nether
4. Savanna
You could use `/droppergroupswap Sea Savanna` to change the order to:
1. Forest
2. Savanna
3. Nether
4. Sea

View File

@ -73,5 +73,11 @@
<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>
</dependencies>
</project>

View File

@ -1,6 +1,7 @@
package net.knarcraft.dropper;
import net.knarcraft.dropper.arena.DropperArenaData;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry;
@ -8,6 +9,9 @@ import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.command.CreateArenaCommand;
import net.knarcraft.dropper.command.EditArenaCommand;
import net.knarcraft.dropper.command.EditArenaTabCompleter;
import net.knarcraft.dropper.command.GroupListCommand;
import net.knarcraft.dropper.command.GroupSetCommand;
import net.knarcraft.dropper.command.GroupSwapCommand;
import net.knarcraft.dropper.command.JoinArenaCommand;
import net.knarcraft.dropper.command.JoinArenaTabCompleter;
import net.knarcraft.dropper.command.LeaveArenaCommand;
@ -21,6 +25,7 @@ import net.knarcraft.dropper.listener.CommandListener;
import net.knarcraft.dropper.listener.DamageListener;
import net.knarcraft.dropper.listener.MoveListener;
import net.knarcraft.dropper.listener.PlayerLeaveListener;
import net.knarcraft.dropper.property.ArenaGameMode;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
@ -76,6 +81,7 @@ public final class Dropper extends JavaPlugin {
public void reload() {
// Load all arenas again
this.arenaHandler.loadArenas();
this.arenaHandler.loadGroups();
}
@Override
@ -86,6 +92,8 @@ public final class Dropper extends JavaPlugin {
ConfigurationSerialization.registerClass(DropperArenaRecordsRegistry.class);
ConfigurationSerialization.registerClass(SerializableUUID.class);
ConfigurationSerialization.registerClass(DropperArenaData.class);
ConfigurationSerialization.registerClass(DropperArenaGroup.class);
ConfigurationSerialization.registerClass(ArenaGameMode.class);
}
@Override
@ -95,14 +103,7 @@ public final class Dropper extends JavaPlugin {
this.playerRegistry = new DropperArenaPlayerRegistry();
this.arenaHandler = new DropperArenaHandler();
this.arenaHandler.loadArenas();
//TODO: Store various information about players' performance, and hook into PlaceholderAPI
//TODO: Possibly implement an optional queue mode, which only allows one player inside one dropper arena at any
// time (to prevent players from pushing each-other)?
//TODO: Store which players have cleared which arenas to keep track of whether the trial game-modes should be
// available
this.arenaHandler.loadGroups();
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new DamageListener(), this);
@ -110,13 +111,16 @@ public final class Dropper extends JavaPlugin {
pluginManager.registerEvents(new PlayerLeaveListener(), this);
pluginManager.registerEvents(new CommandListener(), this);
registerCommand("dropperreload", new ReloadCommand(), null);
registerCommand("droppercreate", new CreateArenaCommand(), null);
registerCommand("dropperlist", new ListArenaCommand(), null);
registerCommand("dropperjoin", new JoinArenaCommand(), new JoinArenaTabCompleter());
registerCommand("dropperleave", new LeaveArenaCommand(), null);
registerCommand("dropperedit", new EditArenaCommand(), new EditArenaTabCompleter());
registerCommand("dropperremove", new RemoveArenaCommand(), new RemoveArenaTabCompleter());
registerCommand("dropperReload", new ReloadCommand(), null);
registerCommand("dropperCreate", new CreateArenaCommand(), null);
registerCommand("dropperList", new ListArenaCommand(), null);
registerCommand("dropperJoin", new JoinArenaCommand(), new JoinArenaTabCompleter());
registerCommand("dropperLeave", new LeaveArenaCommand(), null);
registerCommand("dropperEdit", new EditArenaCommand(), new EditArenaTabCompleter());
registerCommand("dropperRemove", new RemoveArenaCommand(), new RemoveArenaTabCompleter());
registerCommand("dropperGroupSet", new GroupSetCommand(), null);
registerCommand("dropperGroupSwap", new GroupSwapCommand(), null);
registerCommand("dropperGroupList", new GroupListCommand(), null);
}
@Override

View File

@ -1,11 +1,16 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.util.StringSanitizer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
@ -21,47 +26,42 @@ public class DropperArena {
/**
* A name used when listing and storing this arena.
*/
private final @NotNull String arenaName;
private @NotNull String arenaName;
/**
* The location players are teleported to when joining this arena.
*/
private final @NotNull Location spawnLocation;
private @NotNull Location spawnLocation;
/**
* The location players will be sent to when they win or lose the arena. If not set, their entry location should be
* used instead.
*/
private final @Nullable Location exitLocation;
private @Nullable Location exitLocation;
/**
* The velocity in the y-direction to apply to all players in this arena.
*/
private final double playerVerticalVelocity;
private double playerVerticalVelocity;
/**
* The velocity in the x-direction to apply to all players in this arena
*
* <p>This is technically the fly speed</p>
*/
private final float playerHorizontalVelocity;
/**
* The stage number of this arena. If not null, the previous stage number must be cleared before access.
*/
private final @Nullable Integer stage;
private float playerHorizontalVelocity;
/**
* The material of the block players have to hit to win this dropper arena
*/
private final @NotNull Material winBlockType;
private @NotNull Material winBlockType;
/**
* The arena data for this arena
*/
private final DropperArenaData dropperArenaData;
//TODO: It should be possible to get records in sorted order (smallest to largest)
private static DropperArenaHandler dropperArenaHandler = null;
/**
* Instantiates a new dropper arena
@ -72,23 +72,24 @@ public class DropperArena {
* @param exitLocation <p>The location the players are teleported to when exiting the arena, or null</p>
* @param playerVerticalVelocity <p>The velocity to use for players' vertical velocity</p>
* @param playerHorizontalVelocity <p>The velocity to use for players' horizontal velocity (-1 to 1)</p>
* @param stage <p>The stage number of this stage, or null if not limited to stages</p>
* @param winBlockType <p>The material of the block players have to hit to win this dropper arena</p>
* @param dropperArenaData <p>The arena data keeping track of which players have done what in this arena</p>
*/
public DropperArena(@NotNull UUID arenaId, @NotNull String arenaName, @NotNull Location spawnLocation,
@Nullable Location exitLocation, double playerVerticalVelocity, float playerHorizontalVelocity,
@Nullable Integer stage, @NotNull Material winBlockType,
@NotNull DropperArenaData dropperArenaData) {
@NotNull Material winBlockType, @NotNull DropperArenaData dropperArenaData) {
this.arenaId = arenaId;
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = exitLocation;
this.playerVerticalVelocity = playerVerticalVelocity;
this.playerHorizontalVelocity = playerHorizontalVelocity;
this.stage = stage;
this.winBlockType = winBlockType;
this.dropperArenaData = dropperArenaData;
if (dropperArenaHandler == null) {
dropperArenaHandler = Dropper.getInstance().getArenaHandler();
}
}
/**
@ -105,11 +106,15 @@ public class DropperArena {
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = null;
this.playerVerticalVelocity = 1;
this.playerVerticalVelocity = 3.92;
this.playerHorizontalVelocity = 1;
this.stage = null;
this.dropperArenaData = new DropperArenaData(this.arenaId, new DropperArenaRecordsRegistry(this.arenaId),
new HashSet<>());
Map<ArenaGameMode, DropperArenaRecordsRegistry> recordRegistries = new HashMap<>();
for (ArenaGameMode arenaGameMode : ArenaGameMode.values()) {
recordRegistries.put(arenaGameMode, new DropperArenaRecordsRegistry(this.arenaId));
}
this.dropperArenaData = new DropperArenaData(this.arenaId, recordRegistries, new HashMap<>());
this.winBlockType = Material.WATER;
}
@ -183,18 +188,6 @@ public class DropperArena {
return this.playerHorizontalVelocity;
}
/**
* Gets the stage this arena belongs to
*
* <p>It's assumed that arena stages go from 1,2,3,4,... and upwards. If the stage number is set, this arena can
* only be played if all previous stages have been beaten. If not set, however, this arena can be used freely.</p>
*
* @return <p>This arena's stage number</p>
*/
public @Nullable Integer getStage() {
return this.stage;
}
/**
* Gets the type of block a player has to hit to win this arena
*
@ -204,4 +197,135 @@ public class DropperArena {
return this.winBlockType;
}
/**
* Gets this arena's sanitized name
*
* @return <p>This arena's sanitized name</p>
*/
public @NotNull String getArenaNameSanitized() {
return StringSanitizer.sanitizeArenaName(this.getArenaName());
}
/**
* Sets the spawn location for this arena
*
* @param newLocation <p>The new spawn location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setSpawnLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.spawnLocation = newLocation;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the exit location for this arena
*
* @param newLocation <p>The new exit location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setExitLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.exitLocation = newLocation;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the name of this arena
*
* @param arenaName <p>The new name</p>
* @return <p>True if successfully updated</p>
*/
public boolean setName(@NotNull String arenaName) {
if (!arenaName.isBlank()) {
String oldName = this.getArenaNameSanitized();
this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately
dropperArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
dropperArenaHandler.saveArenas();
return true;
} else {
return false;
}
}
/**
* Sets the material of the win block type
*
* <p>The win block type is the type of block a player must hit to win in this arena</p>
*
* @param material <p>The material to set for the win block type</p>
* @return <p>True if successfully updated</p>
*/
public boolean setWinBlockType(@NotNull Material material) {
if (material.isAir() || !material.isBlock()) {
return false;
} else {
this.winBlockType = material;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the horizontal velocity of this arena's players
*
* <p>Note: It's assumed the given value is already bound-checked! (-1 to 1)</p>
*
* @param horizontalVelocity <p>The horizontal velocity to use</p>
* @return <p>True if successfully updated</p>
*/
public boolean setHorizontalVelocity(float horizontalVelocity) {
if (horizontalVelocity < -1 || horizontalVelocity > 1) {
return false;
} else {
this.playerHorizontalVelocity = horizontalVelocity;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the vertical velocity of this arena's players
*
* @param verticalVelocity <p>The vertical velocity to use</p>
* @return <p>True if successfully updated</p>
*/
public boolean setVerticalVelocity(double verticalVelocity) {
if (verticalVelocity <= 0 || verticalVelocity > 100) {
return false;
} else {
this.playerVerticalVelocity = verticalVelocity;
dropperArenaHandler.saveArenas();
return true;
}
}
@Override
public boolean equals(Object other) {
if (!(other instanceof DropperArena otherArena)) {
return false;
}
return this.getArenaNameSanitized().equals(otherArena.getArenaNameSanitized());
}
/**
* Checks whether the given location is valid
*
* @param location <p>The location to validate</p>
* @return <p>False if the location is valid</p>
*/
private boolean isInvalid(Location location) {
World world = location.getWorld();
return world == null || !world.getWorldBorder().isInside(location);
}
}

View File

@ -1,6 +1,8 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.property.ArenaGameMode;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@ -15,34 +17,37 @@ import java.util.UUID;
* Data stored for an arena
*
* @param arenaId <p>The id of the arena this data belongs to</p>
* @param recordsRegistry <p>The records belonging to the arena</p>
* @param recordRegistries <p>The records belonging to the arena</p>
* @param playersCompleted <p>A list of all player that have completed this arena</p>
*/
public record DropperArenaData(@NotNull UUID arenaId, @NotNull DropperArenaRecordsRegistry recordsRegistry,
@NotNull Set<SerializableUUID> playersCompleted) implements ConfigurationSerializable {
public record DropperArenaData(@NotNull UUID arenaId,
@NotNull Map<ArenaGameMode, DropperArenaRecordsRegistry> recordRegistries,
@NotNull Map<ArenaGameMode, Set<UUID>> playersCompleted) implements ConfigurationSerializable {
/**
* Instantiates a new dropper arena data object
*
* @param arenaId <p>The id of the arena this data belongs to</p>
* @param recordsRegistry <p>The registry of this arena's records</p>
* @param playersCompleted <p>The set of ids for players that have cleared this data's arena</p>
* @param recordRegistries <p>The registries of this arena's records</p>
* @param playersCompleted <p>The set of the players that have cleared this arena for each game-mode</p>
*/
public DropperArenaData(@NotNull UUID arenaId, @NotNull DropperArenaRecordsRegistry recordsRegistry,
@NotNull Set<SerializableUUID> playersCompleted) {
public DropperArenaData(@NotNull UUID arenaId,
@NotNull Map<ArenaGameMode, DropperArenaRecordsRegistry> recordRegistries,
@NotNull Map<ArenaGameMode, Set<UUID>> playersCompleted) {
this.arenaId = arenaId;
this.recordsRegistry = recordsRegistry;
this.playersCompleted = new HashSet<>(playersCompleted);
this.recordRegistries = recordRegistries;
this.playersCompleted = new HashMap<>(playersCompleted);
}
/**
* Gets whether the given player has cleared this arena
*
* @param player <p>The player to check</p>
* @param arenaGameMode <p>The game-mode to check for</p>
* @param player <p>The player to check</p>
* @return <p>True if the player has cleared the arena this data belongs to</p>
*/
public boolean hasCompleted(@NotNull Player player) {
return this.playersCompleted.contains(new SerializableUUID(player.getUniqueId()));
public boolean hasNotCompleted(@NotNull ArenaGameMode arenaGameMode, @NotNull Player player) {
return !this.playersCompleted.getOrDefault(arenaGameMode, new HashSet<>()).contains(player.getUniqueId());
}
/**
@ -50,8 +55,18 @@ public record DropperArenaData(@NotNull UUID arenaId, @NotNull DropperArenaRecor
*
* @param player <p>The player that completed this data's arena</p>
*/
public void addCompleted(@NotNull Player player) {
this.playersCompleted.add(new SerializableUUID(player.getUniqueId()));
public boolean addCompleted(@NotNull ArenaGameMode arenaGameMode, @NotNull Player player) {
// Make sure to add an empty set to prevent a NullPointerException
if (!this.playersCompleted.containsKey(arenaGameMode)) {
this.playersCompleted.put(arenaGameMode, new HashSet<>());
}
boolean added = this.playersCompleted.get(arenaGameMode).add(player.getUniqueId());
// Persistently save the completion
if (added) {
Dropper.getInstance().getArenaHandler().saveData(this.arenaId);
}
return added;
}
@NotNull
@ -59,8 +74,18 @@ public record DropperArenaData(@NotNull UUID arenaId, @NotNull DropperArenaRecor
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("arenaId", new SerializableUUID(this.arenaId));
data.put("recordsRegistry", this.recordsRegistry);
data.put("playersCompleted", this.playersCompleted);
data.put("recordsRegistry", this.recordRegistries);
// Convert normal UUIDs to serializable UUIDs
Map<ArenaGameMode, Set<SerializableUUID>> serializablePlayersCompleted = new HashMap<>();
for (ArenaGameMode arenaGameMode : this.playersCompleted.keySet()) {
Set<SerializableUUID> playersCompleted = new HashSet<>();
for (UUID playerCompleted : this.playersCompleted.get(arenaGameMode)) {
playersCompleted.add(new SerializableUUID(playerCompleted));
}
serializablePlayersCompleted.put(arenaGameMode, playersCompleted);
}
data.put("playersCompleted", serializablePlayersCompleted);
return data;
}
@ -73,9 +98,21 @@ public record DropperArenaData(@NotNull UUID arenaId, @NotNull DropperArenaRecor
@SuppressWarnings({"unused", "unchecked"})
public static @NotNull DropperArenaData deserialize(@NotNull Map<String, Object> data) {
SerializableUUID serializableUUID = (SerializableUUID) data.get("arenaId");
DropperArenaRecordsRegistry recordsRegistry = (DropperArenaRecordsRegistry) data.get("recordsRegistry");
Set<SerializableUUID> playersCompleted = (Set<SerializableUUID>) data.get("playersCompleted");
return new DropperArenaData(serializableUUID.uuid(), recordsRegistry, playersCompleted);
Map<ArenaGameMode, DropperArenaRecordsRegistry> recordsRegistry =
(Map<ArenaGameMode, DropperArenaRecordsRegistry>) data.get("recordsRegistry");
Map<ArenaGameMode, Set<SerializableUUID>> playersCompletedData =
(Map<ArenaGameMode, Set<SerializableUUID>>) data.get("playersCompleted");
// Convert the serializable UUIDs to normal UUIDs
Map<ArenaGameMode, Set<UUID>> allPlayersCompleted = new HashMap<>();
for (ArenaGameMode arenaGameMode : playersCompletedData.keySet()) {
Set<UUID> playersCompleted = new HashSet<>();
for (SerializableUUID completedId : playersCompletedData.get(arenaGameMode)) {
playersCompleted.add(completedId.uuid());
}
allPlayersCompleted.put(arenaGameMode, playersCompleted);
}
return new DropperArenaData(serializableUUID.uuid(), recordsRegistry, allPlayersCompleted);
}
}

View File

@ -0,0 +1,253 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.util.StringSanitizer;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
/**
* A sorted group of arenas that must be completed in sequence
*/
public class DropperArenaGroup implements ConfigurationSerializable {
/**
* The unique id for this group of arenas
*/
private final UUID groupId;
/**
* The unique name for this group of arenas
*/
private final String groupName;
/**
* The arenas in this group, ordered from stage 1 to stage n
*/
private final List<UUID> arenas;
/**
* Instantiates a new dropper arena group
*
* @param groupName <p>The name of this group</p>
*/
public DropperArenaGroup(@NotNull String groupName) {
this.groupId = UUID.randomUUID();
this.groupName = groupName;
this.arenas = new ArrayList<>();
}
/**
* Instantiates a new dropper arena group
*
* @param groupId <p>The unique id of this group</p>
* @param groupName <p>The name of this group</p>
* @param arenas <p>The arenas in this group</p>
*/
private DropperArenaGroup(@NotNull UUID groupId, @NotNull String groupName, @NotNull List<UUID> arenas) {
this.groupId = groupId;
this.groupName = groupName;
this.arenas = new ArrayList<>(arenas);
}
/**
* Gets the id of this dropper arena group
*
* @return <p>The id of this group</p>
*/
public @NotNull UUID getGroupId() {
return this.groupId;
}
/**
* Gets the name of this dropper arena group
*
* @return <p>The name of this group</p>
*/
public @NotNull String getGroupName() {
return this.groupName;
}
/**
* Gets the arenas contained in this group in the correct order
*
* @return <p>The ids of the arenas in this group</p>
*/
public @NotNull List<UUID> getArenas() {
return new ArrayList<>(arenas);
}
/**
* Removes the given dropper arena from this group
*
* @param arenaId <p>The id of the dropper arena to remove</p>
*/
public void removeArena(UUID arenaId) {
this.arenas.remove(arenaId);
}
/**
* Adds an arena to the end of this group
*
* @param arenaId <p>The arena to add to this group</p>
*/
public void addArena(UUID arenaId) {
addArena(arenaId, this.arenas.size());
}
/**
* Adds an arena to the end of this group
*
* @param arenaId <p>The arena to add to this group</p>
* @param index <p>The index to put the arena in</p>
*/
public void addArena(UUID arenaId, int index) {
// Make sure we don't have duplicates
if (!this.arenas.contains(arenaId)) {
this.arenas.add(index, arenaId);
}
}
/**
* Checks whether the given player has beaten all arenas in this group on the given game-mode
*
* @param gameMode <p>The game-mode to check</p>
* @param player <p>The player to check</p>
* @return <p>True if the player has beaten all arenas, false otherwise</p>
*/
public boolean hasBeatenAll(ArenaGameMode gameMode, Player player) {
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
for (UUID anArenaId : this.getArenas()) {
DropperArena dropperArena = arenaHandler.getArena(anArenaId);
if (dropperArena == null) {
// The arena would only be null if the arena has been deleted, but not removed from this group
Dropper.getInstance().getLogger().log(Level.WARNING, "The dropper group " + this.getGroupName() +
" contains the arena id " + anArenaId + " which is not a valid arena id!");
continue;
}
if (dropperArena.getData().hasNotCompleted(gameMode, player)) {
return false;
}
}
return true;
}
/**
* Gets whether the given player can play the given arena part of this group, on the given game-mode
*
* @param gameMode <p>The game-mode the player is trying to play</p>
* @param player <p>The player to check</p>
* @param arenaId <p>The id of the arena in this group to check</p>
* @return <p>True if the player is allowed to play the arena</p>
* @throws IllegalArgumentException <p>If checking an arena not in this group</p>
*/
public boolean canPlay(ArenaGameMode gameMode, Player player, UUID arenaId) throws IllegalArgumentException {
if (!this.arenas.contains(arenaId)) {
throw new IllegalArgumentException("Cannot check for playability for arena not in this group!");
}
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
for (UUID anArenaId : this.getArenas()) {
// If the target arena is reached, allow, as all previous arenas must have been cleared
if (arenaId == anArenaId) {
return true;
}
DropperArena dropperArena = arenaHandler.getArena(anArenaId);
if (dropperArena == null) {
// The arena would only be null if the arena has been deleted, but not removed from this group
Dropper.getInstance().getLogger().log(Level.WARNING, String.format("The dropper group %s contains the" +
" arena id %s which is not a valid arena id!", this.getGroupName(), anArenaId));
continue;
}
// This is a lower-numbered arena the player has yet to complete
if (dropperArena.getData().hasNotCompleted(gameMode, player)) {
return false;
}
}
return false;
}
/**
* Gets this group's name, but sanitized
*
* @return <p>The sanitized group name</p>
*/
public @NotNull String getGroupNameSanitized() {
return StringSanitizer.sanitizeArenaName(this.getGroupName());
}
/**
* Swaps the arenas at the given indices
*
* @param index1 <p>The index of the first arena to swap</p>
* @param index2 <p>The index of the second arena to swap</p>
*/
public void swapArenas(int index1, int index2) {
// Change nothing if not a valid request
if (index1 == index2 || index1 < 0 || index2 < 0 || index1 >= this.arenas.size() ||
index2 >= this.arenas.size()) {
return;
}
// Swap the two arena ids
UUID temporaryValue = this.arenas.get(index2);
this.arenas.set(index2, this.arenas.get(index1));
this.arenas.set(index1, temporaryValue);
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("groupId", new SerializableUUID(this.groupId));
data.put("groupName", this.groupName);
List<SerializableUUID> serializableArenas = new ArrayList<>();
for (UUID arenaId : arenas) {
serializableArenas.add(new SerializableUUID(arenaId));
}
data.put("arenas", serializableArenas);
return data;
}
/**
* Deserializes the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized arena group</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static @NotNull DropperArenaGroup deserialize(@NotNull Map<String, Object> data) {
UUID id = ((SerializableUUID) data.get("groupId")).uuid();
String name = (String) data.get("groupName");
List<SerializableUUID> serializableArenas = (List<SerializableUUID>) data.get("arenas");
List<UUID> arenas = new ArrayList<>();
for (SerializableUUID arenaId : serializableArenas) {
arenas.add(arenaId.uuid());
}
return new DropperArenaGroup(id, name, arenas);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof DropperArenaGroup otherGroup)) {
return false;
}
return this.getGroupNameSanitized().equals(otherGroup.getGroupNameSanitized());
}
}

View File

@ -2,13 +2,15 @@ package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.util.ArenaStorageHelper;
import org.bukkit.entity.Player;
import net.knarcraft.dropper.util.StringSanitizer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
@ -18,22 +20,95 @@ import java.util.logging.Level;
public class DropperArenaHandler {
private Map<UUID, DropperArena> arenas = new HashMap<>();
private final Map<Player, Integer> stagesCleared = new HashMap<>();
private Map<UUID, DropperArenaGroup> arenaGroups = new HashMap<>();
private Map<String, UUID> arenaNameLookup = new HashMap<>();
/**
* Tries to register the given stage as cleared
* Gets all arenas that are within a group
*
* @param player <p>The player that cleared a stage</p>
* @param stage <p>The stage the player cleared</p>
* @return <p>True if the player cleared a new stage</p>
* @return <p>All arenas in a group</p>
*/
public boolean registerStageCleared(@NotNull Player player, int stage) {
if ((!stagesCleared.containsKey(player) && stage == 1) || (stagesCleared.containsKey(player) &&
stage == stagesCleared.get(player) + 1)) {
stagesCleared.put(player, stage);
return true;
public @NotNull Set<DropperArena> getArenasInAGroup() {
Set<DropperArena> arenas = new HashSet<>();
for (UUID arenaId : arenaGroups.keySet()) {
arenas.add(this.arenas.get(arenaId));
}
return arenas;
}
/**
* Gets a copy of all dropper groups
*
* @return <p>All dropper groups</p>
*/
public Set<DropperArenaGroup> getAllGroups() {
return new HashSet<>(arenaGroups.values());
}
/**
* Gets the group the given arena belongs to
*
* @param arenaId <p>The id of the arena to get the group of</p>
* @return <p>The group the arena belongs to, or null if not in a group</p>
*/
public @Nullable DropperArenaGroup getGroup(@NotNull UUID arenaId) {
return this.arenaGroups.get(arenaId);
}
/**
* Sets the group for the given arena
*
* @param arenaId <p>The id of the arena to change</p>
* @param arenaGroup <p>The group to add the arena to, or null to remove the current group</p>
*/
public void setGroup(@NotNull UUID arenaId, @Nullable DropperArenaGroup arenaGroup) {
if (arenaGroup == null) {
// No need to remove something non-existing
if (!this.arenaGroups.containsKey(arenaId)) {
return;
}
// Remove the existing group
DropperArenaGroup oldGroup = this.arenaGroups.remove(arenaId);
oldGroup.removeArena(arenaId);
} else {
return false;
// Make sure to remove the arena from the old group's internal tracking
if (this.arenaGroups.containsKey(arenaId)) {
this.arenaGroups.remove(arenaId).removeArena(arenaId);
}
this.arenaGroups.put(arenaId, arenaGroup);
arenaGroup.addArena(arenaId);
}
saveGroups();
}
/**
* Gets the dropper arena group with the given name
*
* @param groupName <p>The name of the group to get</p>
* @return <p>The group, or null if not found</p>
*/
public @Nullable DropperArenaGroup getGroup(String groupName) {
String sanitized = StringSanitizer.sanitizeArenaName(groupName);
for (DropperArenaGroup arenaGroup : this.arenaGroups.values()) {
if (arenaGroup.getGroupNameSanitized().equals(sanitized)) {
return arenaGroup;
}
}
return null;
}
/**
* Replaces an arena's lookup name
*
* @param oldName <p>The arena's old sanitized lookup name</p>
* @param newName <p>The arena's new sanitized lookup name</p>
*/
public void updateLookupName(@NotNull String oldName, @NotNull String newName) {
UUID arenaId = this.arenaNameLookup.remove(oldName);
if (arenaId != null) {
this.arenaNameLookup.put(newName, arenaId);
}
}
@ -44,9 +119,20 @@ public class DropperArenaHandler {
*/
public void addArena(@NotNull DropperArena arena) {
this.arenas.put(arena.getArenaId(), arena);
this.arenaNameLookup.put(arena.getArenaNameSanitized(), arena.getArenaId());
this.saveArenas();
}
/**
* Gets the arena with the given id
*
* @param arenaId <p>The id of the arena to get</p>
* @return <p>The arena, or null if no arena could be found</p>
*/
public @Nullable DropperArena getArena(@NotNull UUID arenaId) {
return this.arenas.get(arenaId);
}
/**
* Gets the arena with the given name
*
@ -54,13 +140,7 @@ public class DropperArenaHandler {
* @return <p>The arena with the given name, or null if not found</p>
*/
public @Nullable DropperArena getArena(@NotNull String arenaName) {
arenaName = ArenaStorageHelper.sanitizeArenaName(arenaName);
for (DropperArena arena : arenas.values()) {
if (ArenaStorageHelper.sanitizeArenaName(arena.getArenaName()).equals(arenaName)) {
return arena;
}
}
return null;
return this.arenas.get(this.arenaNameLookup.get(StringSanitizer.sanitizeArenaName(arenaName)));
}
/**
@ -80,6 +160,8 @@ public class DropperArenaHandler {
public void removeArena(@NotNull DropperArena arena) {
Dropper.getInstance().getPlayerRegistry().removeForArena(arena);
this.arenas.remove(arena.getArenaId());
this.arenaNameLookup.remove(arena.getArenaNameSanitized());
this.arenaGroups.remove(arena.getArenaId());
this.saveArenas();
}
@ -97,6 +179,33 @@ public class DropperArenaHandler {
}
}
/**
* Saves all current dropper groups to disk
*/
public void saveGroups() {
try {
ArenaStorageHelper.saveDropperArenaGroups(new HashSet<>(this.arenaGroups.values()));
} catch (IOException e) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Unable to save current arena groups! " +
"Data loss can occur!");
Dropper.getInstance().getLogger().log(Level.SEVERE, e.getMessage());
}
}
/**
* Loads all dropper groups from disk
*/
public void loadGroups() {
Set<DropperArenaGroup> arenaGroups = ArenaStorageHelper.loadDropperArenaGroups();
Map<UUID, DropperArenaGroup> arenaGroupMap = new HashMap<>();
for (DropperArenaGroup arenaGroup : arenaGroups) {
for (UUID arenaId : arenaGroup.getArenas()) {
arenaGroupMap.put(arenaId, arenaGroup);
}
}
this.arenaGroups = arenaGroupMap;
}
/**
* Saves all current arenas to disk
*/
@ -115,6 +224,13 @@ public class DropperArenaHandler {
*/
public void loadArenas() {
this.arenas = ArenaStorageHelper.loadArenas();
// Save a map from arena name to arena id for improved performance
this.arenaNameLookup = new HashMap<>();
for (Map.Entry<UUID, DropperArena> arena : this.arenas.entrySet()) {
String sanitizedName = arena.getValue().getArenaNameSanitized();
this.arenaNameLookup.put(sanitizedName, arena.getKey());
}
}
}

View File

@ -17,8 +17,8 @@ import java.util.stream.Stream;
public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
private final UUID arenaId;
private final Map<SerializableUUID, Integer> leastDeaths;
private final Map<SerializableUUID, Long> shortestTimeMilliSeconds;
private final @NotNull Map<UUID, Number> leastDeaths;
private final @NotNull Map<UUID, Number> shortestTimeMilliSeconds;
/**
* Instantiates a new empty records registry
@ -35,8 +35,8 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* @param leastDeaths <p>The existing least death records to use</p>
* @param shortestTimeMilliSeconds <p>The existing leash time records to use</p>
*/
public DropperArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Map<SerializableUUID, Integer> leastDeaths,
@NotNull Map<SerializableUUID, Long> shortestTimeMilliSeconds) {
private DropperArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Map<UUID, Integer> leastDeaths,
@NotNull Map<UUID, Long> shortestTimeMilliSeconds) {
this.arenaId = arenaId;
this.leastDeaths = new HashMap<>(leastDeaths);
this.shortestTimeMilliSeconds = new HashMap<>(shortestTimeMilliSeconds);
@ -47,8 +47,12 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
*
* @return <p>Existing death records</p>
*/
public Map<SerializableUUID, Integer> getLeastDeathsRecords() {
return new HashMap<>(this.leastDeaths);
public Map<UUID, Integer> getLeastDeathsRecords() {
Map<UUID, Integer> leastDeathRecords = new HashMap<>();
for (Map.Entry<UUID, Number> entry : this.leastDeaths.entrySet()) {
leastDeathRecords.put(entry.getKey(), entry.getValue().intValue());
}
return leastDeathRecords;
}
/**
@ -56,8 +60,12 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
*
* @return <p>Existing time records</p>
*/
public Map<SerializableUUID, Long> getShortestTimeMilliSecondsRecords() {
return new HashMap<>(this.shortestTimeMilliSeconds);
public Map<UUID, Long> getShortestTimeMilliSecondsRecords() {
Map<UUID, Long> leastTimeRecords = new HashMap<>();
for (Map.Entry<UUID, Number> entry : this.shortestTimeMilliSeconds.entrySet()) {
leastTimeRecords.put(entry.getKey(), entry.getValue().longValue());
}
return leastTimeRecords;
}
/**
@ -68,29 +76,7 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* @return <p>The result explaining what type of record was achieved</p>
*/
public @NotNull RecordResult registerDeathRecord(@NotNull UUID playerId, int deaths) {
RecordResult result;
Stream<Map.Entry<SerializableUUID, Integer>> records = leastDeaths.entrySet().stream();
SerializableUUID serializableUUID = new SerializableUUID(playerId);
if (records.allMatch((entry) -> deaths < entry.getValue())) {
// If the given value is less than all other values, that's a world record!
result = RecordResult.WORLD_RECORD;
leastDeaths.put(serializableUUID, deaths);
save();
} else if (leastDeaths.containsKey(serializableUUID) && deaths < leastDeaths.get(serializableUUID)) {
// If the given value is less than the player's previous value, that's a personal best!
result = RecordResult.PERSONAL_BEST;
leastDeaths.put(serializableUUID, deaths);
save();
} else {
// Make sure to save the record if this is the user's first attempt
if (!leastDeaths.containsKey(serializableUUID)) {
save();
}
result = RecordResult.NONE;
}
return result;
return registerRecord(leastDeaths, playerId, deaths);
}
/**
@ -101,30 +87,7 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* @return <p>The result explaining what type of record was achieved</p>
*/
public @NotNull RecordResult registerTimeRecord(@NotNull UUID playerId, long milliseconds) {
RecordResult result;
Stream<Map.Entry<SerializableUUID, Long>> records = shortestTimeMilliSeconds.entrySet().stream();
SerializableUUID serializableUUID = new SerializableUUID(playerId);
if (records.allMatch((entry) -> milliseconds < entry.getValue())) {
//If the given value is less than all other values, that's a world record!
result = RecordResult.WORLD_RECORD;
shortestTimeMilliSeconds.put(serializableUUID, milliseconds);
save();
} else if (shortestTimeMilliSeconds.containsKey(serializableUUID) &&
milliseconds < shortestTimeMilliSeconds.get(serializableUUID)) {
//If the given value is less than the player's previous value, that's a personal best!
result = RecordResult.PERSONAL_BEST;
shortestTimeMilliSeconds.put(serializableUUID, milliseconds);
save();
} else {
// Make sure to save the record if this is the user's first attempt
if (!shortestTimeMilliSeconds.containsKey(serializableUUID)) {
save();
}
result = RecordResult.NONE;
}
return result;
return registerRecord(shortestTimeMilliSeconds, playerId, milliseconds);
}
/**
@ -134,13 +97,58 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
Dropper.getInstance().getArenaHandler().saveData(this.arenaId);
}
/**
* Registers a new record if applicable
*
* @param existingRecords <p>The map of existing records to use</p>
* @param playerId <p>The id of the player that potentially achieved a record</p>
* @param amount <p>The amount of whatever the player achieved</p>
* @return <p>The result of the player's record attempt</p>
*/
private @NotNull RecordResult registerRecord(@NotNull Map<UUID, Number> existingRecords, @NotNull UUID playerId,
Number amount) {
RecordResult result;
Stream<Map.Entry<UUID, Number>> records = existingRecords.entrySet().stream();
long amountLong = amount.longValue();
if (records.allMatch((entry) -> amountLong < entry.getValue().longValue())) {
// If the given value is less than all other values, that's a world record!
result = RecordResult.WORLD_RECORD;
existingRecords.put(playerId, amount);
save();
} else if (existingRecords.containsKey(playerId) && amountLong < existingRecords.get(playerId).longValue()) {
// If the given value is less than the player's previous value, that's a personal best!
result = RecordResult.PERSONAL_BEST;
existingRecords.put(playerId, amount);
save();
} else {
// Make sure to save the record if this is the user's first attempt
if (!existingRecords.containsKey(playerId)) {
save();
}
result = RecordResult.NONE;
}
return result;
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("arenaId", new SerializableUUID(this.arenaId));
data.put("leastDeaths", this.leastDeaths);
data.put("shortestTime", this.shortestTimeMilliSeconds);
Map<SerializableUUID, Number> leastDeaths = new HashMap<>();
for (Map.Entry<UUID, Number> entry : this.leastDeaths.entrySet()) {
leastDeaths.put(new SerializableUUID(entry.getKey()), entry.getValue());
}
data.put("leastDeaths", leastDeaths);
Map<SerializableUUID, Number> shortestTimeMilliSeconds = new HashMap<>();
for (Map.Entry<UUID, Number> entry : this.shortestTimeMilliSeconds.entrySet()) {
shortestTimeMilliSeconds.put(new SerializableUUID(entry.getKey()), entry.getValue());
}
data.put("shortestTime", shortestTimeMilliSeconds);
return data;
}
@ -155,10 +163,19 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
UUID arenaId = ((SerializableUUID) data.get("arenaId")).uuid();
Map<SerializableUUID, Integer> leastDeathsData =
(Map<SerializableUUID, Integer>) data.getOrDefault("leastDeaths", new HashMap<>());
Map<SerializableUUID, Long> shortestTimeMillisecondsData =
(Map<SerializableUUID, Long>) data.getOrDefault("shortestTime", new HashMap<>());
Map<UUID, Integer> leastDeaths = new HashMap<>();
for (Map.Entry<SerializableUUID, Integer> entry : leastDeathsData.entrySet()) {
leastDeaths.put(entry.getKey().uuid(), entry.getValue());
}
return new DropperArenaRecordsRegistry(arenaId, leastDeathsData, shortestTimeMillisecondsData);
Map<SerializableUUID, Number> shortestTimeMillisecondsData =
(Map<SerializableUUID, Number>) data.getOrDefault("shortestTime", new HashMap<>());
Map<UUID, Long> shortestTimeMilliseconds = new HashMap<>();
for (Map.Entry<SerializableUUID, Number> entry : shortestTimeMillisecondsData.entrySet()) {
shortestTimeMilliseconds.put(entry.getKey().uuid(), entry.getValue().longValue());
}
return new DropperArenaRecordsRegistry(arenaId, leastDeaths, shortestTimeMilliseconds);
}
}

View File

@ -37,11 +37,20 @@ public class DropperArenaSession {
this.deaths = 0;
this.startTime = System.currentTimeMillis();
this.entryState = new PlayerEntryState(player);
this.entryState = new PlayerEntryState(player, gameMode);
// Make the player fly to improve mobility in the air
this.entryState.setArenaState(this.arena.getPlayerHorizontalVelocity());
}
/**
* Gets the game-mode the player is playing in this session
*
* @return <p>The game-mode for this session</p>
*/
public @NotNull ArenaGameMode getGameMode() {
return this.gameMode;
}
/**
* Gets the state of the player when they joined the session
*
@ -61,17 +70,10 @@ public class DropperArenaSession {
// Check for, and display, records
registerRecord();
//TODO: Give reward?
// Register and announce any cleared stages
Integer arenaStage = this.arena.getStage();
if (arenaStage != null) {
boolean clearedNewStage = Dropper.getInstance().getArenaHandler().registerStageCleared(this.player, arenaStage);
if (clearedNewStage) {
this.player.sendMessage("You cleared stage " + arenaStage + "!");
}
// Mark the arena as cleared
if (this.arena.getData().addCompleted(this.gameMode, this.player)) {
this.player.sendMessage("You cleared the arena!");
}
this.player.sendMessage("You won!");
// Teleport the player out of the arena
@ -108,27 +110,44 @@ public class DropperArenaSession {
* Registers the player's record if necessary, and prints record information to the player
*/
private void registerRecord() {
DropperArenaRecordsRegistry recordsRegistry = this.arena.getData().recordsRegistry();
RecordResult recordResult = switch (this.gameMode) {
case LEAST_TIME -> recordsRegistry.registerTimeRecord(this.player.getUniqueId(),
System.currentTimeMillis() - this.startTime);
case LEAST_DEATHS -> recordsRegistry.registerDeathRecord(this.player.getUniqueId(), this.deaths);
case DEFAULT -> RecordResult.NONE;
};
switch (recordResult) {
case WORLD_RECORD -> this.player.sendMessage("You just set a new record for this arena!");
case PERSONAL_BEST -> this.player.sendMessage("You just got a new personal record!");
DropperArenaRecordsRegistry recordsRegistry = this.arena.getData().recordRegistries().get(this.gameMode);
long timeElapsed = System.currentTimeMillis() - this.startTime;
announceRecord(recordsRegistry.registerTimeRecord(this.player.getUniqueId(), timeElapsed), "time");
announceRecord(recordsRegistry.registerDeathRecord(this.player.getUniqueId(), this.deaths), "least deaths");
}
/**
* Announces a record set by this player
*
* @param recordResult <p>The result of the record</p>
* @param type <p>The type of record set (time or deaths)</p>
*/
private void announceRecord(@NotNull RecordResult recordResult, @NotNull String type) {
if (recordResult == RecordResult.NONE) {
return;
}
// Gets a string representation of the played game-mode
String gameModeString = switch (this.gameMode) {
case DEFAULT -> "default";
case INVERTED -> "inverted";
case RANDOM_INVERTED -> "random";
};
String recordString = "You just set a %s on the %s game-mode!";
recordString = switch (recordResult) {
case WORLD_RECORD -> String.format(recordString, "new %s record", gameModeString);
case PERSONAL_BEST -> String.format(recordString, "personal %s record", gameModeString);
default -> throw new IllegalStateException("Unexpected value: " + recordResult);
};
player.sendMessage(String.format(recordString, type));
}
/**
* Triggers a loss for the player playing in this session
*/
public void triggerLoss() {
// Add to the death count if playing the least-deaths game-mode
if (this.gameMode == ArenaGameMode.LEAST_DEATHS) {
this.deaths++;
}
this.deaths++;
//Teleport the player back to the top
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), true, false);
this.entryState.setArenaState(this.arena.getPlayerHorizontalVelocity());

View File

@ -1,8 +1,10 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.property.ArenaGameMode;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* The state of a player before entering a dropper arena
@ -17,21 +19,23 @@ public class PlayerEntryState {
private final boolean originalAllowFlight;
private final boolean originalInvulnerable;
private final boolean originalIsSwimming;
private final ArenaGameMode arenaGameMode;
/**
* Instantiates a new player state
*
* @param player <p>The player whose state should be stored</p>
*/
public PlayerEntryState(Player player) {
public PlayerEntryState(@NotNull Player player, @NotNull ArenaGameMode arenaGameMode) {
this.player = player;
this.entryLocation = player.getLocation();
this.entryLocation = player.getLocation().clone();
this.originalFlySpeed = player.getFlySpeed();
this.originalIsFlying = player.isFlying();
this.originalGameMode = player.getGameMode();
this.originalAllowFlight = player.getAllowFlight();
this.originalInvulnerable = player.isInvulnerable();
this.originalIsSwimming = player.isSwimming();
this.arenaGameMode = arenaGameMode;
}
/**
@ -42,9 +46,15 @@ public class PlayerEntryState {
public void setArenaState(float horizontalVelocity) {
this.player.setAllowFlight(true);
this.player.setFlying(true);
this.player.setFlySpeed(horizontalVelocity);
this.player.setGameMode(GameMode.ADVENTURE);
this.player.setSwimming(false);
// If playing on the inverted game-mode, negate the horizontal velocity to swap the controls
if (arenaGameMode == ArenaGameMode.INVERTED) {
this.player.setFlySpeed(-horizontalVelocity);
} else {
this.player.setFlySpeed(horizontalVelocity);
}
}
/**

View File

@ -2,6 +2,7 @@ package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.util.StringSanitizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -26,15 +27,20 @@ public class CreateArenaCommand implements CommandExecutor {
return false;
}
DropperArena existingArena = Dropper.getInstance().getArenaHandler().getArena(arguments[0]);
// Remove known characters that are likely to cause trouble if used in an arena name
String arenaName = StringSanitizer.removeUnwantedCharacters(arguments[0]);
// An arena name is required
if (arenaName.isBlank()) {
return false;
}
DropperArena existingArena = Dropper.getInstance().getArenaHandler().getArena(arenaName);
if (existingArena != null) {
commandSender.sendMessage("There already exists a dropper arena with that name!");
return false;
}
// Remove known characters that are likely to cause trouble if used in an arena name
String arenaName = arguments[0].replaceAll("[§ :=&]", "");
DropperArena arena = new DropperArena(arenaName, player.getLocation());
Dropper.getInstance().getArenaHandler().addArena(arena);
commandSender.sendMessage("The arena was successfully created!");

View File

@ -2,6 +2,9 @@ package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.property.ArenaEditableProperty;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -16,7 +19,7 @@ public class EditArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(commandSender instanceof Player)) {
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
return false;
}
@ -31,9 +34,127 @@ public class EditArenaCommand implements CommandExecutor {
return false;
}
//TODO: If an arena name and a property is given, display the current value
//TODO: If an arena name, a property and a value is given, check if it's valid, and update the property
return false;
ArenaEditableProperty editableProperty = ArenaEditableProperty.getFromArgumentString(arguments[1]);
if (editableProperty == null) {
commandSender.sendMessage("Unknown property specified.");
return false;
}
String currentValueFormat = "Current value of %s is: %s";
if (arguments.length < 3) {
// Print the current value of the property
String value = editableProperty.getCurrentValueAsString(specifiedArena);
commandSender.sendMessage(String.format(currentValueFormat, editableProperty.getArgumentString(), value));
return true;
} else {
boolean successful = changeValue(specifiedArena, editableProperty, arguments[2], player);
if (successful) {
player.sendMessage(String.format("Property %s changed to: %s", editableProperty, arguments[2]));
} else {
player.sendMessage("Unable to change the property. Make sure your input is valid!");
}
return successful;
}
}
/**
* Changes the given property to the given value
*
* @param arena <p>The arena to change the property for</p>
* @param property <p>The property to change</p>
* @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>
*/
private boolean changeValue(@NotNull DropperArena arena, @NotNull ArenaEditableProperty property,
@NotNull String value, @NotNull Player player) {
return switch (property) {
case WIN_BLOCK_TYPE -> arena.setWinBlockType(parseMaterial(value));
case HORIZONTAL_VELOCITY -> arena.setHorizontalVelocity(sanitizeHorizontalVelocity(value));
case VERTICAL_VELOCITY -> arena.setVerticalVelocity(sanitizeVerticalVelocity(value));
case SPAWN_LOCATION -> arena.setSpawnLocation(parseLocation(player, value));
case NAME -> arena.setName(value);
case EXIT_LOCATION -> arena.setExitLocation(parseLocation(player, value));
};
}
/**
* Sanitizes the player's specified vertical velocity
*
* @param velocityString <p>The string to parse into a velocity</p>
* @return <p>The parsed velocity, defaulting to 0.5 if not parse-able</p>
*/
private double sanitizeVerticalVelocity(@NotNull String velocityString) {
// Vertical velocity should not be negative, as it would make the player go upwards. There is technically not a
// max speed limit, but setting it too high makes the arena unplayable
double velocity;
try {
velocity = Double.parseDouble(velocityString);
} catch (NumberFormatException exception) {
velocity = 3.92;
}
// Require at least speed of 0.001, and at most 75 blocks/s
return Math.min(Math.max(velocity, 0.001), 75);
}
/**
* Sanitizes the user's specified horizontal velocity
*
* @param velocityString <p>The string to parse into a velocity</p>
* @return <p>The parsed velocity, defaulting to 1 if not parse-able</p>
*/
private float sanitizeHorizontalVelocity(@NotNull String velocityString) {
// Horizontal velocity is valid between -1 and 1, where negative values swaps directions
float velocity;
try {
velocity = Float.parseFloat(velocityString);
} catch (NumberFormatException exception) {
velocity = 1;
}
// Make sure the velocity isn't exactly 0
if (velocity == 0) {
velocity = 0.5f;
}
// If outside bonds, choose the most extreme value
return Math.min(Math.max(0.1f, velocity), 1);
}
/**
* Parses the given location string
*
* @param player <p>The player changing a location</p>
* @param locationString <p>The location string to parse</p>
* @return <p>The parsed location, or the player's location if not parse-able</p>
*/
private @NotNull Location parseLocation(Player player, String locationString) {
if ((locationString.trim() + ",").matches("([0-9]+.?[0-9]*,){3}")) {
String[] parts = locationString.split(",");
Location newLocation = player.getLocation().clone();
newLocation.setX(Double.parseDouble(parts[0].trim()));
newLocation.setY(Double.parseDouble(parts[1].trim()));
newLocation.setZ(Double.parseDouble(parts[2].trim()));
return newLocation;
} else {
return player.getLocation().clone();
}
}
/**
* Parses the given material name
*
* @param materialName <p>The material name to parse</p>
* @return <p>The parsed material, or AIR if not valid</p>
*/
private @NotNull Material parseMaterial(String materialName) {
Material material = Material.matchMaterial(materialName);
if (material == null) {
material = Material.AIR;
}
return material;
}
}

View File

@ -1,11 +1,13 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
@ -16,9 +18,16 @@ public class EditArenaTabCompleter implements TabCompleter {
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
//TODO: Tab-complete existing arena names
//TODO: If an arena name is given, tab-complete change-able properties
return null;
if (args.length == 1) {
return TabCompleteHelper.getArenas();
} else if (args.length == 2) {
return TabCompleteHelper.getArenaProperties();
} else if (args.length == 3) {
//TODO: Tab-complete possible values for the given property
return null;
} else {
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,93 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* The command for listing groups and the stages within
*/
public class GroupListCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
if (arguments.length == 0) {
displayExistingGroups(arenaHandler, commandSender);
return true;
} else if (arguments.length == 1) {
return displayOrderedArenaNames(arenaHandler, commandSender, arguments[0]);
} else {
return false;
}
}
/**
* Displays all currently existing dropper arena groups
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the groups to</p>
*/
private void displayExistingGroups(@NotNull DropperArenaHandler arenaHandler, @NotNull CommandSender sender) {
StringBuilder builder = new StringBuilder("Dropper arena groups:").append("\n");
arenaHandler.getAllGroups().stream().sorted().forEachOrdered((group) ->
builder.append(group.getGroupName()).append("\n"));
sender.sendMessage(builder.toString());
}
/**
* Displays the ordered stages in a specified group to the specified command sender
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the stages to</p>
* @param groupName <p>The name of the group to display stages for</p>
* @return <p>True if the stages were successfully displayed</p>
*/
private boolean displayOrderedArenaNames(@NotNull DropperArenaHandler arenaHandler, @NotNull CommandSender sender,
@NotNull String groupName) {
DropperArenaGroup arenaGroup = arenaHandler.getGroup(groupName);
if (arenaGroup == null) {
sender.sendMessage("Unable to find the specified group!");
return false;
}
// Send a list of all stages (arenas in the group)
StringBuilder builder = new StringBuilder(groupName).append("'s stages:").append("\n");
int counter = 1;
for (UUID arenaId : arenaGroup.getArenas()) {
DropperArena arena = arenaHandler.getArena(arenaId);
if (arena != null) {
builder.append(counter++).append(". ").append(arena.getArenaName()).append("\n");
}
}
sender.sendMessage(builder.toString());
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
List<String> groupNames = new ArrayList<>();
for (DropperArenaGroup group : Dropper.getInstance().getArenaHandler().getAllGroups()) {
groupNames.add(group.getGroupName());
}
return groupNames;
} else {
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,79 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.util.StringSanitizer;
import net.knarcraft.dropper.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The command for setting the group of an arena
*/
public class GroupSetCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length < 2) {
return false;
}
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
DropperArena specifiedArena = arenaHandler.getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
return false;
}
String groupName = StringSanitizer.removeUnwantedCharacters(arguments[1]);
if (groupName.isBlank()) {
return false;
}
DropperArenaGroup arenaGroup;
if (groupName.equalsIgnoreCase("null") || groupName.equalsIgnoreCase("none")) {
arenaGroup = null;
} else {
arenaGroup = arenaHandler.getGroup(groupName);
if (arenaGroup == null) {
arenaGroup = new DropperArenaGroup(groupName);
}
}
arenaHandler.setGroup(specifiedArena.getArenaId(), arenaGroup);
commandSender.sendMessage("The arena's group has been updated");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompleteHelper.getArenas();
} else if (arguments.length == 2) {
List<String> possibleValues = new ArrayList<>();
possibleValues.add("none");
possibleValues.add("GroupName");
for (DropperArenaGroup group : Dropper.getInstance().getArenaHandler().getAllGroups()) {
possibleValues.add(group.getGroupName());
}
return possibleValues;
} else {
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,102 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* The command for swapping the order of two arenas in a group
*/
public class GroupSwapCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length < 2) {
return false;
}
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
DropperArena arena1 = arenaHandler.getArena(arguments[0]);
if (arena1 == null) {
commandSender.sendMessage("Unable to find the first specified dropper arena.");
return false;
}
DropperArena arena2 = arenaHandler.getArena(arguments[1]);
if (arena2 == null) {
commandSender.sendMessage("Unable to find the second specified dropper arena.");
return false;
}
DropperArenaGroup arena1Group = arenaHandler.getGroup(arena1.getArenaId());
DropperArenaGroup arena2Group = arenaHandler.getGroup(arena2.getArenaId());
if (arena1Group == null || !arena1Group.equals(arena2Group)) {
commandSender.sendMessage("You cannot swap arenas in different groups!");
return false;
}
arena1Group.swapArenas(arena1Group.getArenas().indexOf(arena1.getArenaId()),
arena1Group.getArenas().indexOf(arena2.getArenaId()));
commandSender.sendMessage("The arenas have been swapped!");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
if (arguments.length == 1) {
List<String> arenaNames = new ArrayList<>();
for (DropperArena dropperArena : arenaHandler.getArenasInAGroup()) {
arenaNames.add(dropperArena.getArenaName());
}
return arenaNames;
} else if (arguments.length == 2) {
return getArenaNamesInSameGroup(arguments[0]);
} else {
return new ArrayList<>();
}
}
/**
* Gets the names of all arenas in the same group as the specified arena
*
* @param arenaName <p>The name of the specified arena</p>
* @return <p>The names of the arenas in the same group</p>
*/
private List<String> getArenaNamesInSameGroup(String arenaName) {
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
DropperArena arena1 = arenaHandler.getArena(arenaName);
if (arena1 == null) {
return new ArrayList<>();
}
// Only display other arenas in the selected group
List<String> arenaNames = new ArrayList<>();
DropperArenaGroup group = arenaHandler.getGroup(arena1.getArenaId());
if (group == null) {
return new ArrayList<>();
}
for (UUID arenaId : group.getArenas()) {
DropperArena arena = arenaHandler.getArena(arenaId);
if (arena != null && arena.getArenaId() != arena1.getArenaId()) {
arenaNames.add(arena.getArenaName());
}
}
return arenaNames;
}
}

View File

@ -2,6 +2,7 @@ package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.property.ArenaGameMode;
@ -69,7 +70,18 @@ public class JoinArenaCommand implements CommandExecutor {
gameMode = ArenaGameMode.DEFAULT;
}
//TODO: Check if the arena has been beaten if the non-default game-mode has been chosen
// Make sure the player has beaten the necessary levels
DropperArenaGroup arenaGroup = Dropper.getInstance().getArenaHandler().getGroup(specifiedArena.getArenaId());
if (arenaGroup != null && !doGroupChecks(specifiedArena, arenaGroup, gameMode, player)) {
return false;
}
// Make sure the player has beaten the arena once in normal mode before playing another mode
if (gameMode != ArenaGameMode.DEFAULT &&
specifiedArena.getData().hasNotCompleted(ArenaGameMode.DEFAULT, player)) {
player.sendMessage("You must complete this arena in normal mode first!");
return false;
}
// Register the player's session
DropperArenaSession newSession = new DropperArenaSession(specifiedArena, player, gameMode);
@ -90,4 +102,32 @@ public class JoinArenaCommand implements CommandExecutor {
}
}
/**
* Performs necessary check for the given arena's group
*
* @param dropperArena <p>The arena the player is trying to join</p>
* @param arenaGroup <p>The arena group the arena belongs to</p>
* @param arenaGameMode <p>The game-mode the player selected</p>
* @param player <p>The the player trying to join the arena</p>
* @return <p>False if any checks failed</p>
*/
private boolean doGroupChecks(@NotNull DropperArena dropperArena, @NotNull DropperArenaGroup arenaGroup,
@NotNull ArenaGameMode arenaGameMode, @NotNull Player player) {
// Require that players beat all arenas in the group in the normal game-mode before trying challenge modes
if (arenaGameMode != ArenaGameMode.DEFAULT) {
if (!arenaGroup.hasBeatenAll(ArenaGameMode.DEFAULT, player)) {
player.sendMessage("You have not yet beaten all arenas in this group!");
return false;
}
}
// Require that the player has beaten the previous arena on the same game-mode before trying this one
if (!arenaGroup.canPlay(arenaGameMode, player, dropperArena.getArenaId())) {
player.sendMessage("You have not yet beaten the previous arena!");
return false;
}
return true;
}
}

View File

@ -23,8 +23,8 @@ public class JoinArenaTabCompleter implements TabCompleter {
} else if (arguments.length == 2) {
List<String> gameModes = new ArrayList<>();
gameModes.add("default");
gameModes.add("deaths");
gameModes.add("time");
gameModes.add("inverted");
gameModes.add("random");
return gameModes;
} else {
return new ArrayList<>();

View File

@ -22,8 +22,6 @@ public class ListArenaCommand implements TabExecutor {
for (String arenaName : TabCompleteHelper.getArenas()) {
sender.sendMessage(arenaName);
}
//TODO: Allow displaying information about each arena (possibly admin-only)
return true;
}

View File

@ -3,6 +3,7 @@ package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.property.ArenaGameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
@ -11,7 +12,9 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;
@ -40,6 +43,27 @@ public class MoveListener implements Listener {
return;
}
// Only do block type checking if the block beneath the player changes
if (event.getFrom().getBlock() != event.getTo().getBlock() &&
checkForSpecialBlock(arenaSession, event.getTo())) {
return;
}
//Updates the player's velocity to the one set by the arena
updatePlayerVelocity(arenaSession);
}
/**
* Check if the player in the session is triggering a block with a special significance
*
* <p>This basically looks for the win block, or whether the player is hitting a solid block.</p>
*
* @param arenaSession <p>The arena session to check for</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>
*/
private boolean checkForSpecialBlock(DropperArenaSession arenaSession, Location toLocation) {
/* This decides how far inside a non-solid block the player must go before detection triggers. The closer to -1
it is, the more accurate it will seem to the player, but the likelihood of not detecting the hit decreases */
double liquidDepth = -0.8;
@ -47,31 +71,27 @@ public class MoveListener implements Listener {
likelihood of detecting the hit decreases, but the immersion increases. */
double solidDepth = 0.2;
// Only do block type checking if the block beneath the player changes
if (event.getFrom().getBlock() != event.getTo().getBlock()) {
// Check if the player enters water
Material winBlockType = arenaSession.getArena().getWinBlockType();
// For water, only trigger when the player enters the water, but trigger earlier for everything else
double depth = !winBlockType.isSolid() ? liquidDepth : solidDepth;
for (Block block : getBlocksBeneathLocation(event.getTo(), depth)) {
if (block.getType() == winBlockType) {
arenaSession.triggerWin();
return;
}
}
// Check if the player is about to hit a non-air and non-liquid block
for (Block block : getBlocksBeneathLocation(event.getTo(), solidDepth)) {
if (!block.getType().isAir() && block.getType() != Material.STRUCTURE_VOID &&
block.getType() != Material.WATER && block.getType() != Material.LAVA) {
arenaSession.triggerLoss();
return;
}
// Check if the player enters water
Material winBlockType = arenaSession.getArena().getWinBlockType();
// For water, only trigger when the player enters the water, but trigger earlier for everything else
double depth = !winBlockType.isSolid() ? liquidDepth : solidDepth;
for (Block block : getBlocksBeneathLocation(toLocation, depth)) {
if (block.getType() == winBlockType) {
arenaSession.triggerWin();
return true;
}
}
//Updates the player's velocity to the one set by the arena
updatePlayerVelocity(arenaSession);
// 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.getType() != Material.STRUCTURE_VOID &&
block.getType() != Material.WATER && block.getType() != Material.LAVA) {
arenaSession.triggerLoss();
return true;
}
}
return false;
}
/**
@ -95,12 +115,40 @@ public class MoveListener implements Listener {
*
* @param session <p>The session to update the velocity for</p>
*/
private void updatePlayerVelocity(DropperArenaSession session) {
private void updatePlayerVelocity(@NotNull DropperArenaSession session) {
Player player = session.getPlayer();
Vector playerVelocity = player.getVelocity();
double arenaVelocity = session.getArena().getPlayerVerticalVelocity();
Vector newVelocity = new Vector(playerVelocity.getX(), -arenaVelocity, playerVelocity.getZ());
player.setVelocity(newVelocity);
// Toggle the direction of the player's flying, as necessary
toggleFlyInversion(session);
}
/**
* Toggles the player's flying direction inversion if playing on the random game-mode
*
* @param session <p>The session to possibly invert flying for</p>
*/
private void toggleFlyInversion(@NotNull DropperArenaSession session) {
if (session.getGameMode() != ArenaGameMode.RANDOM_INVERTED) {
return;
}
Player player = session.getPlayer();
float horizontalVelocity = session.getArena().getPlayerHorizontalVelocity();
float secondsBetweenToggle = 7;
int seconds = Calendar.getInstance().get(Calendar.SECOND);
/*
* A trick to make the inversion change after a customizable amount of seconds
* If the quotient of dividing the current number of seconds with the set amount of seconds is even, invert.
* So, if the number of seconds between toggles is 5, that would mean for the first 5 seconds, the flying would
* be inverted. Once 5 seconds have passed, the quotient becomes 1, which is odd, so the flying is no longer
* inverted. After 10 seconds, the quotient is 2, which is even, and inverts the flying.
*/
boolean invert = Math.floor(seconds / secondsBetweenToggle) % 2 == 0;
player.setFlySpeed(invert ? -horizontalVelocity : horizontalVelocity);
}
}

View File

@ -0,0 +1,92 @@
package net.knarcraft.dropper.property;
import net.knarcraft.dropper.arena.DropperArena;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
/**
* All editable properties of a dropper arena
*/
public enum ArenaEditableProperty {
/**
* The name of the arena
*/
NAME("name", DropperArena::getArenaName),
/**
* The arena's spawn location
*/
SPAWN_LOCATION("spawnLocation", (arena) -> String.valueOf(arena.getSpawnLocation())),
/**
* The arena's exit location
*/
EXIT_LOCATION("exitLocation", (arena) -> String.valueOf(arena.getExitLocation())),
/**
* The arena's vertical velocity
*/
VERTICAL_VELOCITY("verticalVelocity", (arena) -> String.valueOf(arena.getPlayerVerticalVelocity())),
/**
* The arena's horizontal velocity
*/
HORIZONTAL_VELOCITY("horizontalVelocity", (arena) -> String.valueOf(arena.getPlayerHorizontalVelocity())),
/**
* The arena's win block type
*/
WIN_BLOCK_TYPE("winBlockType", (arena) -> arena.getWinBlockType().toString()),
;
private final @NotNull String argumentString;
private final Function<DropperArena, String> currentValueProvider;
/**
* Instantiates a new arena editable property
*
* @param argumentString <p>The argument string used to specify this property</p>
*/
ArenaEditableProperty(@NotNull String argumentString, Function<DropperArena, String> currentValueProvider) {
this.argumentString = argumentString;
this.currentValueProvider = currentValueProvider;
}
/**
* Gets the string representation of this property's current value
*
* @param arena <p>The arena to check the value for</p>
* @return <p>The current value as a string</p>
*/
public String getCurrentValueAsString(DropperArena arena) {
return this.currentValueProvider.apply(arena);
}
/**
* Gets the argument string used to specify this property
*
* @return <p>The argument string</p>
*/
public @NotNull String getArgumentString() {
return this.argumentString;
}
/**
* Gets the editable property corresponding to the given argument string
*
* @param argumentString <p>The argument string used to specify an editable property</p>
* @return <p>The corresponding editable property, or null if not found</p>
*/
public static @Nullable ArenaEditableProperty getFromArgumentString(String argumentString) {
for (ArenaEditableProperty property : ArenaEditableProperty.values()) {
if (property.argumentString.equalsIgnoreCase(argumentString)) {
return property;
}
}
return null;
}
}

View File

@ -1,11 +1,15 @@
package net.knarcraft.dropper.property;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* A representation of possible arena game-modes
*/
public enum ArenaGameMode {
public enum ArenaGameMode implements ConfigurationSerializable {
/**
* The default game-mode. Failing once throws the player out.
@ -13,14 +17,14 @@ public enum ArenaGameMode {
DEFAULT,
/**
* The least-deaths game-mode. Player plays until they manage to win. The number of deaths is recorded.
* A game-mode where the player's directional buttons are inverted
*/
LEAST_DEATHS,
INVERTED,
/**
* The least-time game-mode. Player plays until they manage to win. The total time of the session is recorded.
* A game-mode which swaps between normal and inverted controls on a set schedule of a few seconds
*/
LEAST_TIME,
RANDOM_INVERTED,
;
/**
@ -31,13 +35,32 @@ public enum ArenaGameMode {
*/
public static @NotNull ArenaGameMode matchGamemode(@NotNull String gameMode) {
String sanitized = gameMode.trim().toLowerCase();
if (sanitized.matches("(least)?deaths?")) {
return ArenaGameMode.LEAST_DEATHS;
} else if (sanitized.matches("(least)?time")) {
return ArenaGameMode.LEAST_TIME;
if (sanitized.matches("(invert(ed)?|inverse)")) {
return ArenaGameMode.INVERTED;
} else if (sanitized.matches("rand(om)?")) {
return ArenaGameMode.RANDOM_INVERTED;
} else {
return ArenaGameMode.DEFAULT;
}
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("name", this.name());
return data;
}
/**
* Deserializes the arena game-mode specified by the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized arena game-mode</p>
*/
@SuppressWarnings("unused")
public static ArenaGameMode deserialize(Map<String, Object> data) {
return ArenaGameMode.valueOf((String) data.get("name"));
}
}

View File

@ -37,11 +37,6 @@ public enum ArenaStorageKey {
*/
PLAYER_HORIZONTAL_VELOCITY("arenaPlayerHorizontalVelocity"),
/**
* The key for this arena's stage
*/
STAGE("arenaStage"),
/**
* The key for the type of this arena's win block
*/

View File

@ -3,9 +3,11 @@ package net.knarcraft.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaData;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry;
import net.knarcraft.dropper.container.SerializableMaterial;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.property.ArenaStorageKey;
import org.bukkit.Location;
import org.bukkit.Material;
@ -19,6 +21,7 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
@ -28,7 +31,9 @@ import java.util.logging.Level;
public final class ArenaStorageHelper {
private final static String arenasConfigurationSection = "arenas";
private final static String groupsConfigurationSection = "groups";
private static final File arenaFile = new File(Dropper.getInstance().getDataFolder(), "arenas.yml");
private static final File groupFile = new File(Dropper.getInstance().getDataFolder(), "groups.yml");
private static final File arenaDataFolder = new File(Dropper.getInstance().getDataFolder(), "arena_data");
private ArenaStorageHelper() {
@ -36,7 +41,46 @@ public final class ArenaStorageHelper {
}
/**
* Saves the given arenas to the given file
* Saves the given dropper arena groups
*
* @param arenaGroups <p>The arena groups to save</p>
* @throws IOException <p>If unable to write to the file</p>
*/
public static void saveDropperArenaGroups(@NotNull Set<DropperArenaGroup> arenaGroups) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection groupSection = configuration.createSection(groupsConfigurationSection);
for (DropperArenaGroup arenaGroup : arenaGroups) {
groupSection.set(arenaGroup.getGroupId().toString(), arenaGroup);
}
configuration.save(groupFile);
}
/**
* Loads all existing dropper arena groups
*
* @return <p>The loaded arena groups</p>
*/
public static @NotNull Set<DropperArenaGroup> loadDropperArenaGroups() {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(groupFile);
ConfigurationSection groupSection = configuration.getConfigurationSection(groupsConfigurationSection);
//If no such section exists, it must be the case that there is no data to load
if (groupSection == null) {
return new HashSet<>();
}
Set<DropperArenaGroup> arenaGroups = new HashSet<>();
for (String sectionName : groupSection.getKeys(false)) {
arenaGroups.add((DropperArenaGroup) groupSection.get(sectionName));
}
return arenaGroups;
}
/**
* Saves the given arenas
*
* @param arenas <p>The arenas to save</p>
* @throws IOException <p>If unable to write to the file</p>
@ -47,15 +91,13 @@ public final class ArenaStorageHelper {
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(sanitizeArenaName(
arena.getArenaName()));
@NotNull ConfigurationSection configSection = arenaSection.createSection(arena.getArenaId().toString());
configSection.set(ArenaStorageKey.ID.getKey(), new SerializableUUID(arena.getArenaId()));
configSection.set(ArenaStorageKey.NAME.getKey(), arena.getArenaName());
configSection.set(ArenaStorageKey.SPAWN_LOCATION.getKey(), arena.getSpawnLocation());
configSection.set(ArenaStorageKey.EXIT_LOCATION.getKey(), arena.getExitLocation());
configSection.set(ArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey(), arena.getPlayerVerticalVelocity());
configSection.set(ArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey(), arena.getPlayerHorizontalVelocity());
configSection.set(ArenaStorageKey.STAGE.getKey(), arena.getStage());
configSection.set(ArenaStorageKey.WIN_BLOCK_TYPE.getKey(), new SerializableMaterial(arena.getWinBlockType()));
saveArenaData(arena.getData());
}
@ -63,7 +105,7 @@ public final class ArenaStorageHelper {
}
/**
* Loads all arenas from the given file
* Loads all arenas
*
* @return <p>The loaded arenas, or null if the arenas configuration section is missing.</p>
*/
@ -108,7 +150,6 @@ public final class ArenaStorageHelper {
double verticalVelocity = configurationSection.getDouble(ArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey());
float horizontalVelocity = sanitizeHorizontalVelocity((float) configurationSection.getDouble(
ArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey()));
Integer stage = (Integer) configurationSection.get(ArenaStorageKey.STAGE.getKey());
SerializableMaterial winBlockType = (SerializableMaterial) configurationSection.get(
ArenaStorageKey.WIN_BLOCK_TYPE.getKey());
@ -124,21 +165,16 @@ public final class ArenaStorageHelper {
DropperArenaData arenaData = loadArenaData(arenaId);
if (arenaData == null) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Unable to load arena data for " + arenaId);
arenaData = new DropperArenaData(arenaId, new DropperArenaRecordsRegistry(arenaId), new HashSet<>());
Map<ArenaGameMode, DropperArenaRecordsRegistry> recordRegistries = new HashMap<>();
for (ArenaGameMode arenaGameMode : ArenaGameMode.values()) {
recordRegistries.put(arenaGameMode, new DropperArenaRecordsRegistry(arenaId));
}
arenaData = new DropperArenaData(arenaId, recordRegistries, new HashMap<>());
}
return new DropperArena(arenaId, arenaName, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity,
stage, winBlockType.material(), arenaData);
}
/**
* Sanitizes an arena name for usage as a YAML key
*
* @param arenaName <p>The arena name to sanitize</p>
* @return <p>The sanitized arena name</p>
*/
public static @NotNull String sanitizeArenaName(@NotNull String arenaName) {
return arenaName.toLowerCase().trim().replaceAll(" ", "_");
winBlockType.material(), arenaData);
}
/**

View File

@ -0,0 +1,37 @@
package net.knarcraft.dropper.util;
import org.jetbrains.annotations.NotNull;
/**
* A helper-class for sanitizing strings
*/
public final class StringSanitizer {
private StringSanitizer() {
}
/**
* Removes unwanted characters from a string
*
* <p>This basically removes character that have a special meaning in YML, or ones that cannot be used in the
* chat.</p>
*
* @param input <p>The string to remove from</p>
* @return <p>The string with the unwanted characters removed</p>
*/
public static @NotNull String removeUnwantedCharacters(@NotNull String input) {
return input.replaceAll("[§ :=&]", "");
}
/**
* Sanitizes an arena name for usage as a YAML key
*
* @param arenaName <p>The arena name to sanitize</p>
* @return <p>The sanitized arena name</p>
*/
public static @NotNull String sanitizeArenaName(@NotNull String arenaName) {
return arenaName.toLowerCase().trim().replaceAll(" ", "_");
}
}

View File

@ -2,6 +2,7 @@ package net.knarcraft.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.property.ArenaEditableProperty;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@ -29,4 +30,17 @@ public final class TabCompleteHelper {
return arenaNames;
}
/**
* Gets the argument strings of all arena properties
*
* @return <p>All arena properties</p>
*/
public static @NotNull List<String> getArenaProperties() {
List<String> arenaProperties = new ArrayList<>();
for (ArenaEditableProperty property : ArenaEditableProperty.values()) {
arenaProperties.add(property.getArgumentString());
}
return arenaProperties;
}
}

View File

@ -4,48 +4,77 @@ main: net.knarcraft.dropper.Dropper
api-version: 1.19
description: A plugin for dropper mini-games
# Note to self: Aliases must be lowercase!
commands:
dropperreload:
dropperGroupSet:
aliases:
- dgset
permission: dropper.edit
usage: |
/<command> <arena> <group>
- The group will be created if it doesn't already exist
- Use "none" or "null" as the group to release the arena from its group
description: Sets the group of the given arena
dropperGroupSwap:
aliases:
- dgswap
permission: dropper.edit
usage: |
/<command> <arena1> <arena2>
- The two arenas must be in the same group
description: Swaps the order of two arenas in the same group
dropperGroupList:
aliases:
- dglist
permission: dropper.edit
usage: |
/<command> [group]
- Existing groups will be listed if used without an argument
- Supplying a group shows the group's arenas
description: Lists existing groups and their arenas
dropperReload:
aliases:
- dreload
permission: dropper.admin
usage: /<command>
description: Reloads all data from disk
dropperlist:
dropperList:
aliases:
- dlist
permission: dropper.join
usage: /<command>
description: Used to list all current dropper arenas
dropperjoin:
dropperJoin:
aliases:
- djoin
permission: dropper.join
usage: |
/<command> <arena> [mode]
Mode can be used to select challenge modes which can be played after beating the arena.
deaths = A least-deaths competitive game-mode
time = A shortest-time competitive game-mode
- Mode can be used to select challenge modes which can be played after beating the arena.
- inverted = A game-mode where the WASD buttons are inverted
- random = A game-mode where the WASD buttons toggle between being inverted and not
description: Used to join a dropper arena
dropperleave:
dropperLeave:
aliases:
- dleave
permission: dropper.join
usage: /<command>
description: Used to leave the current dropper arena
droppercreate:
dropperCreate:
aliases:
- dcreate
permission: dropper.create
usage: /<command> (Details not finalized)
usage: |
/<command> <arena> <property> [new value]
- Valid properties: name, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity, winBlockType
description: Used to create a new dropper arena
dropperedit:
dropperEdit:
aliases:
- dedit
permission: dropper.edit
usage: /<command> (Details not finalized)
description: Used to edit an existing dropper arena
dropperremove:
dropperRemove:
aliases:
- dremove
permission: dropper.remove

View File

@ -0,0 +1,50 @@
package net.knarcraft.dropper.arena;
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 DropperArenaGroupTest {
@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());
}
}