7 Commits

Author SHA1 Message Date
9a56f58f2f Adds a reload command 2023-03-26 17:56:49 +02:00
592f53ec9e Implements persistent storage of records 2023-03-26 17:42:55 +02:00
49eb0ac82c Adds some missing command argument checks 2023-03-26 15:52:54 +02:00
0c58860026 Adds a lot of small improvements
Makes the type of block players have to hit to win configurable
Separates the velocity option into vertical and horizontal velocities
Reduces some redundancy when getting an arena from an arena name
Partially implements the list command
Tries to improve the handling of players exiting in the middle of a session
Adds missing comments to ArenaStorageKey
2023-03-25 23:18:03 +01:00
6385b4c5e8 Implements the remove command 2023-03-25 12:35:15 +01:00
14572de102 Improves collision detection
This change majorly improves the hit-detection for blocks hit by a player while dropping.
2023-03-25 12:14:25 +01:00
fba75d2c3f Abuses flight mode for better in-air control 2023-03-24 16:22:52 +01:00
23 changed files with 643 additions and 149 deletions

View File

@ -2,6 +2,7 @@ package net.knarcraft.dropper;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry;
import net.knarcraft.dropper.command.CreateArenaCommand;
import net.knarcraft.dropper.command.EditArenaCommand;
import net.knarcraft.dropper.command.EditArenaTabCompleter;
@ -9,13 +10,18 @@ import net.knarcraft.dropper.command.JoinArenaCommand;
import net.knarcraft.dropper.command.JoinArenaTabCompleter;
import net.knarcraft.dropper.command.LeaveArenaCommand;
import net.knarcraft.dropper.command.ListArenaCommand;
import net.knarcraft.dropper.command.ReloadCommand;
import net.knarcraft.dropper.command.RemoveArenaCommand;
import net.knarcraft.dropper.command.RemoveArenaTabCompleter;
import net.knarcraft.dropper.container.SerializableMaterial;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.listener.DamageListener;
import net.knarcraft.dropper.listener.MoveListener;
import net.knarcraft.dropper.listener.PlayerLeaveListener;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
@ -60,6 +66,23 @@ public final class Dropper extends JavaPlugin {
return this.playerRegistry;
}
/**
* Reloads all configurations and data from disk
*/
public void reload() {
// Load all arenas again
this.arenaHandler.loadArenas();
}
@Override
public void onLoad() {
super.onLoad();
// Register serialization classes
ConfigurationSerialization.registerClass(SerializableMaterial.class);
ConfigurationSerialization.registerClass(DropperArenaRecordsRegistry.class);
ConfigurationSerialization.registerClass(SerializableUUID.class);
}
@Override
public void onEnable() {
// Plugin startup logic
@ -83,12 +106,13 @@ public final class Dropper extends JavaPlugin {
pluginManager.registerEvents(new MoveListener(), this);
pluginManager.registerEvents(new PlayerLeaveListener(), 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(), null);
registerCommand("dropperremove", new RemoveArenaCommand(), new RemoveArenaTabCompleter());
}
@Override

View File

@ -1,6 +1,7 @@
package net.knarcraft.dropper.arena;
import org.bukkit.Location;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -28,7 +29,14 @@ public class DropperArena {
/**
* The velocity in the y-direction to apply to all players in this arena.
*/
private final double playerVelocity;
private final 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 double playerHorizontalVelocity;
/**
* The stage number of this arena. If not null, the previous stage number must be cleared before access.
@ -40,26 +48,36 @@ public class DropperArena {
*/
private final @NotNull DropperArenaRecordsRegistry recordsRegistry;
/**
* The material of the block players have to hit to win this dropper arena
*/
private final @NotNull Material winBlockType;
//TODO: Store records for this arena (maps with player->deaths/time). It should be possible to get those in sorted
// order (smallest to largest)
/**
* Instantiates a new dropper arena
*
* @param arenaName <p>The name of the arena</p>
* @param spawnLocation <p>The location players spawn in when entering the arena</p>
* @param exitLocation <p>The location the players are teleported to when exiting the arena, or null</p>
* @param playerVelocity <p>The velocity multiplier to use for players' velocity</p>
* @param stage <p>The stage number of this stage, or null if not limited to stages</p>
* @param recordsRegistry <p>The registry keeping track of all of this arena's records</p>
* @param arenaName <p>The name of the arena</p>
* @param spawnLocation <p>The location players spawn in when entering the arena</p>
* @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</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 recordsRegistry <p>The registry keeping track of all of this arena's records</p>
*/
public DropperArena(@NotNull String arenaName, @NotNull Location spawnLocation, @Nullable Location exitLocation,
double playerVelocity, @Nullable Integer stage, @NotNull DropperArenaRecordsRegistry recordsRegistry) {
double playerVerticalVelocity, double playerHorizontalVelocity, @Nullable Integer stage, @NotNull Material winBlockType,
@NotNull DropperArenaRecordsRegistry recordsRegistry) {
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = exitLocation;
this.playerVelocity = playerVelocity;
this.playerVerticalVelocity = playerVerticalVelocity;
this.playerHorizontalVelocity = playerHorizontalVelocity;
this.stage = stage;
this.winBlockType = winBlockType;
this.recordsRegistry = recordsRegistry;
}
@ -76,9 +94,11 @@ public class DropperArena {
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = null;
this.playerVelocity = 1;
this.playerVerticalVelocity = 1;
this.playerHorizontalVelocity = 1;
this.stage = null;
this.recordsRegistry = new DropperArenaRecordsRegistry();
this.winBlockType = Material.WATER;
}
/**
@ -120,15 +140,26 @@ public class DropperArena {
}
/**
* Gets the velocity for players in this arena
* Gets the vertical velocity for players in this arena
*
* <p>The velocity is the multiplier used to define players' dropping speed in this dropper arena. 1.0 is the normal
* falling speed. 0.5 is half speed. 2 is double speed etc.</p>
* <p>This velocity will be set on the negative y-axis, for all players in this arena.</p>
*
* @return <p>Players' velocity in this arena</p>
*/
public double getPlayerVelocity() {
return this.playerVelocity;
public double getPlayerVerticalVelocity() {
return this.playerVerticalVelocity;
}
/**
* Gets the horizontal for players in this arena
*
* <p>This will be used for players' fly-speed in this arena</p>
*
* @return <p>Players' velocity in this arena</p>
*/
public double getPlayerHorizontalVelocity() {
return this.playerHorizontalVelocity;
}
/**
@ -143,6 +174,13 @@ public class DropperArena {
return this.stage;
}
//TODO: Add the appropriate getters/setters and other methods
/**
* Gets the type of block a player has to hit to win this arena
*
* @return <p>The kind of block players must hit</p>
*/
public @NotNull Material getWinBlockType() {
return this.winBlockType;
}
}

View File

@ -4,6 +4,7 @@ import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.util.ArenaStorageHelper;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
@ -50,6 +51,22 @@ public class DropperArenaHandler {
this.saveArenas();
}
/**
* Gets the arena with the given name
*
* @param arenaName <p>The arena to get</p>
* @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) {
if (ArenaStorageHelper.sanitizeArenaName(arena.getArenaName()).equals(arenaName)) {
return arena;
}
}
return null;
}
/**
* Gets all known arenas
*
@ -65,6 +82,7 @@ public class DropperArenaHandler {
* @param arena <p>The arena to remove</p>
*/
public void removeArena(@NotNull DropperArena arena) {
Dropper.getInstance().getPlayerRegistry().removeForArena(arena);
this.arenas.remove(arena);
this.saveArenas();
}

View File

@ -43,4 +43,19 @@ public class DropperArenaPlayerRegistry {
return this.arenaPlayers.getOrDefault(playerId, null);
}
/**
* Removes all active sessions for the given arena
*
* @param arena <p>The arena to remove sessions for</p>
*/
public void removeForArena(DropperArena arena) {
for (Map.Entry<UUID, DropperArenaSession> entry : this.arenaPlayers.entrySet()) {
if (entry.getValue().getArena() == arena) {
// Kick the player gracefully
entry.getValue().triggerQuit();
this.arenaPlayers.remove(entry.getKey());
}
}
}
}

View File

@ -1,20 +1,23 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.property.RecordResult;
import org.bukkit.entity.Player;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;
/**
* A registry keeping track of all records
*/
public class DropperArenaRecordsRegistry {
public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
private final Map<Player, Integer> leastDeaths;
private final Map<Player, Long> shortestTimeMilliSeconds;
private final Map<SerializableUUID, Integer> leastDeaths;
private final Map<SerializableUUID, Long> shortestTimeMilliSeconds;
/**
* Instantiates a new empty records registry
@ -30,8 +33,8 @@ public class DropperArenaRecordsRegistry {
* @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 Map<Player, Integer> leastDeaths,
@NotNull Map<Player, Long> shortestTimeMilliSeconds) {
public DropperArenaRecordsRegistry(@NotNull Map<SerializableUUID, Integer> leastDeaths,
@NotNull Map<SerializableUUID, Long> shortestTimeMilliSeconds) {
this.leastDeaths = new HashMap<>(leastDeaths);
this.shortestTimeMilliSeconds = new HashMap<>(shortestTimeMilliSeconds);
}
@ -41,7 +44,7 @@ public class DropperArenaRecordsRegistry {
*
* @return <p>Existing death records</p>
*/
public Map<Player, Integer> getLeastDeathsRecords() {
public Map<SerializableUUID, Integer> getLeastDeathsRecords() {
return new HashMap<>(this.leastDeaths);
}
@ -50,29 +53,32 @@ public class DropperArenaRecordsRegistry {
*
* @return <p>Existing time records</p>
*/
public Map<Player, Long> getShortestTimeMilliSecondsRecords() {
public Map<SerializableUUID, Long> getShortestTimeMilliSecondsRecords() {
return new HashMap<>(this.shortestTimeMilliSeconds);
}
/**
* Registers a new deaths-record
*
* @param player <p>The player that performed the records</p>
* @param deaths <p>The number of deaths suffered before the player finished the arena</p>
* @param playerId <p>The id of the player that performed the records</p>
* @param deaths <p>The number of deaths suffered before the player finished the arena</p>
* @return <p>The result explaining what type of record was achieved</p>
*/
public @NotNull RecordResult registerDeathRecord(@NotNull Player player, int deaths) {
public @NotNull RecordResult registerDeathRecord(@NotNull UUID playerId, int deaths) {
RecordResult result;
Stream<Map.Entry<Player, Integer>> records = leastDeaths.entrySet().stream();
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(player, deaths);
} else if (leastDeaths.containsKey(player) && deaths < leastDeaths.get(player)) {
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(player, deaths);
leastDeaths.put(serializableUUID, deaths);
save();
} else {
result = RecordResult.NONE;
}
@ -83,22 +89,26 @@ public class DropperArenaRecordsRegistry {
/**
* Registers a new time-record
*
* @param player <p>The player that performed the records</p>
* @param playerId <p>The id of the player that performed the records</p>
* @param milliseconds <p>The number of milliseconds it took the player to finish the dropper arena</p>
* @return <p>The result explaining what type of record was achieved</p>
*/
public @NotNull RecordResult registerTimeRecord(@NotNull Player player, long milliseconds) {
public @NotNull RecordResult registerTimeRecord(@NotNull UUID playerId, long milliseconds) {
RecordResult result;
Stream<Map.Entry<Player, Long>> records = shortestTimeMilliSeconds.entrySet().stream();
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(player, milliseconds);
} else if (shortestTimeMilliSeconds.containsKey(player) && milliseconds < shortestTimeMilliSeconds.get(player)) {
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(player, milliseconds);
shortestTimeMilliSeconds.put(serializableUUID, milliseconds);
save();
} else {
result = RecordResult.NONE;
}
@ -106,4 +116,36 @@ public class DropperArenaRecordsRegistry {
return result;
}
/**
* Saves changed records
*/
private void save() {
Dropper.getInstance().getArenaHandler().saveArenas();
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("leastDeaths", this.leastDeaths);
data.put("shortestTime", this.shortestTimeMilliSeconds);
return data;
}
/**
* Deserializes the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized records registry</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static DropperArenaRecordsRegistry deserialize(Map<String, Object> data) {
Map<SerializableUUID, Integer> leastDeathsData =
(Map<SerializableUUID, Integer>) data.getOrDefault("leastDeaths", new HashMap<>());
Map<SerializableUUID, Long> shortestTimeMillisecondsData =
(Map<SerializableUUID, Long>) data.getOrDefault("shortestTime", new HashMap<>());
return new DropperArenaRecordsRegistry(leastDeathsData, shortestTimeMillisecondsData);
}
}

View File

@ -21,6 +21,7 @@ public class DropperArenaSession {
private final @NotNull Location entryLocation;
private int deaths;
private final long startTime;
private final float playersOriginalFlySpeed;
/**
* Instantiates a new dropper arena session
@ -37,19 +38,20 @@ public class DropperArenaSession {
this.deaths = 0;
this.startTime = System.currentTimeMillis();
this.entryLocation = player.getLocation();
// Prevent Spigot interference when traveling at high velocities
// Make the player fly to improve mobility in the air
player.setAllowFlight(true);
player.setFlying(true);
this.playersOriginalFlySpeed = player.getFlySpeed();
player.setFlySpeed((float) this.arena.getPlayerHorizontalVelocity());
}
/**
* Triggers a win for the player playing in this session
*/
public void triggerWin() {
// Remove this session from game sessions to stop listeners from fiddling more with the player
removeSession();
// No longer allow the player to avoid fly checks
player.setAllowFlight(false);
// Stop this session
stopSession();
// Check for, and display, records
registerRecord();
@ -57,15 +59,15 @@ public class DropperArenaSession {
//TODO: Give reward?
// Register and announce any cleared stages
Integer arenaStage = arena.getStage();
Integer arenaStage = this.arena.getStage();
if (arenaStage != null) {
boolean clearedNewStage = Dropper.getInstance().getArenaHandler().registerStageCleared(player, arenaStage);
boolean clearedNewStage = Dropper.getInstance().getArenaHandler().registerStageCleared(this.player, arenaStage);
if (clearedNewStage) {
player.sendMessage("You cleared stage " + arenaStage + "!");
this.player.sendMessage("You cleared stage " + arenaStage + "!");
}
}
player.sendMessage("You won!");
this.player.sendMessage("You won!");
// Teleport the player out of the arena
teleportToExit();
@ -77,12 +79,12 @@ public class DropperArenaSession {
private void teleportToExit() {
// Teleport the player out of the arena
Location exitLocation;
if (arena.getExitLocation() != null) {
exitLocation = arena.getExitLocation();
if (this.arena.getExitLocation() != null) {
exitLocation = this.arena.getExitLocation();
} else {
exitLocation = entryLocation;
exitLocation = this.entryLocation;
}
PlayerTeleporter.teleportPlayer(player, exitLocation, true);
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true);
}
/**
@ -101,16 +103,16 @@ public class DropperArenaSession {
* Registers the player's record if necessary, and prints record information to the player
*/
private void registerRecord() {
DropperArenaRecordsRegistry recordsRegistry = arena.getRecordsRegistry();
RecordResult recordResult = switch (gameMode) {
case LEAST_TIME -> recordsRegistry.registerTimeRecord(player,
System.currentTimeMillis() - startTime);
case LEAST_DEATHS -> recordsRegistry.registerDeathRecord(player, deaths);
DropperArenaRecordsRegistry recordsRegistry = this.arena.getRecordsRegistry();
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 -> player.sendMessage("You just set a new record for this arena!");
case PERSONAL_BEST -> player.sendMessage("You just got a new personal record!");
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!");
}
}
@ -119,27 +121,38 @@ public class DropperArenaSession {
*/
public void triggerLoss() {
// Add to the death count if playing the least-deaths game-mode
if (gameMode == ArenaGameMode.LEAST_DEATHS) {
deaths++;
if (this.gameMode == ArenaGameMode.LEAST_DEATHS) {
this.deaths++;
}
//Teleport the player back to the top
PlayerTeleporter.teleportPlayer(player, arena.getSpawnLocation(), true);
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), true);
}
/**
* Triggers a quit for the player playing in this session
*/
public void triggerQuit() {
// Remove this session from game sessions to stop listeners from fiddling more with the player
removeSession();
// No longer allow the player to avoid fly checks
player.setAllowFlight(false);
// Stop this session
stopSession();
// Teleport the player out of the arena
teleportToExit();
player.sendMessage("You quit the arena!");
}
/**
* Stops this session, and disables flight mode
*/
private void stopSession() {
// Remove this session from game sessions to stop listeners from fiddling more with the player
removeSession();
// Remove flight mode
this.player.setFlySpeed(this.playersOriginalFlySpeed);
this.player.setFlying(false);
this.player.setAllowFlight(false);
}
/**
* Gets the arena this session is being played in
*

View File

@ -2,7 +2,6 @@ package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.util.ArenaStorageHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -27,17 +26,14 @@ public class CreateArenaCommand implements CommandExecutor {
return false;
}
String arenaName = arguments[0];
String sanitized = ArenaStorageHelper.sanitizeArenaName(arenaName);
for (DropperArena arena : Dropper.getInstance().getArenaHandler().getArenas()) {
if (sanitized.equals(ArenaStorageHelper.sanitizeArenaName(arena.getArenaName()))) {
commandSender.sendMessage("There already exists a dropper arena with that name!");
return false;
}
DropperArena existingArena = Dropper.getInstance().getArenaHandler().getArena(arguments[0]);
if (existingArena != null) {
commandSender.sendMessage("There already exists a dropper arena with that name!");
return false;
}
//TODO: Make sure the arena name doesn't contain any unwanted characters
// 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);

View File

@ -1,8 +1,11 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
@ -12,8 +15,22 @@ public class EditArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] strings) {
//TODO: Make sure the console cannot run this
@NotNull String[] arguments) {
if (!(commandSender instanceof Player)) {
commandSender.sendMessage("This command must be used by a player");
return false;
}
if (arguments.length < 2) {
return false;
}
DropperArena specifiedArena = Dropper.getInstance().getArenaHandler().getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
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;

View File

@ -5,7 +5,6 @@ import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.util.ArenaStorageHelper;
import net.knarcraft.dropper.util.PlayerTeleporter;
import org.bukkit.GameMode;
import org.bukkit.command.Command;
@ -45,14 +44,7 @@ public class JoinArenaCommand implements CommandExecutor {
}
// Make sure the arena exists
String arenaName = ArenaStorageHelper.sanitizeArenaName(arguments[0]);
DropperArena specifiedArena = null;
for (DropperArena arena : Dropper.getInstance().getArenaHandler().getArenas()) {
if (ArenaStorageHelper.sanitizeArenaName(arena.getArenaName()).equals(arenaName)) {
specifiedArena = arena;
break;
}
}
DropperArena specifiedArena = Dropper.getInstance().getArenaHandler().getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
return false;

View File

@ -1,7 +1,6 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
@ -18,14 +17,10 @@ public class JoinArenaTabCompleter implements TabCompleter {
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (args.length == 1) {
List<String> arenaNames = new ArrayList<>();
for (DropperArena dropperArena : Dropper.getInstance().getArenaHandler().getArenas()) {
arenaNames.add(dropperArena.getArenaName());
}
return arenaNames;
} else if (args.length == 2) {
@NotNull String label, @NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompleteHelper.getArenas();
} else if (arguments.length == 2) {
List<String> gameModes = new ArrayList<>();
gameModes.add("default");
gameModes.add("deaths");

View File

@ -3,15 +3,19 @@ package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArenaSession;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The command used to leave the current dropper arena
*/
public class LeaveArenaCommand implements CommandExecutor {
public class LeaveArenaCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@ -21,7 +25,8 @@ public class LeaveArenaCommand implements CommandExecutor {
return false;
}
DropperArenaSession existingSession = Dropper.getInstance().getPlayerRegistry().getArenaSession(player.getUniqueId());
DropperArenaSession existingSession = Dropper.getInstance().getPlayerRegistry().getArenaSession(
player.getUniqueId());
if (existingSession == null) {
commandSender.sendMessage("You are not in a dropper arena!");
return false;
@ -31,4 +36,11 @@ public class LeaveArenaCommand implements CommandExecutor {
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
return new ArrayList<>();
}
}

View File

@ -1,20 +1,37 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
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;
/**
* A command for listing existing dropper arenas
*/
public class ListArenaCommand implements CommandExecutor {
public class ListArenaCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
//TODO: List all existing arenas, and possibly information about a specified arena
return false;
@NotNull String[] arguments) {
sender.sendMessage("Dropper arenas:");
for (String arenaName : TabCompleteHelper.getArenas()) {
sender.sendMessage(arenaName);
}
//TODO: Allow displaying information about each arena (possibly admin-only)
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
return new ArrayList<>();
}
}

View File

@ -0,0 +1,33 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
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 reloading the plugin
*/
public class ReloadCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
Dropper.getInstance().reload();
commandSender.sendMessage("Plugin reloaded!");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
return new ArrayList<>();
}
}

View File

@ -1,5 +1,7 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -12,12 +14,23 @@ public class RemoveArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] strings) {
//TODO: Make sure to kick any playing players if the arena is currently in use, by triggering their sessions'
// triggerQuit() method
//TODO: Remove the arena from DropperArenaHandler
//TODO: Notify the user of success
return false;
@NotNull String[] arguments) {
// Abort if no name was specified
if (arguments.length < 1) {
return false;
}
// Get the specified arena
DropperArena targetArena = Dropper.getInstance().getArenaHandler().getArena(arguments[0]);
if (targetArena == null) {
commandSender.sendMessage("Unable to find the specified arena");
return false;
}
// Remove the arena
Dropper.getInstance().getArenaHandler().removeArena(targetArena);
commandSender.sendMessage("The specified arena has been successfully removed");
return true;
}
}

View File

@ -0,0 +1,29 @@
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;
/**
* The tab-completer for the remove arena command
*/
public class RemoveArenaTabCompleter implements TabCompleter {
@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 {
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,37 @@
package net.knarcraft.dropper.container;
import org.bukkit.Material;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* A material container able to be serialized
*
* @param material <p>The material stored by this record</p>
*/
public record SerializableMaterial(Material material) implements ConfigurationSerializable {
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("name", material.name());
return data;
}
/**
* Deserializes a serialized material
*
* @param data <p>The serialized data</p>
* @return <p>The deserialized material</p>
*/
@SuppressWarnings("unused")
public static SerializableMaterial deserialize(Map<String, Object> data) {
Material material = Material.matchMaterial((String) data.getOrDefault("name", "AIR"));
return new SerializableMaterial(material);
}
}

View File

@ -0,0 +1,50 @@
package net.knarcraft.dropper.container;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* A UUID container able to be serialized
*
* @param uuid <p>The UUID stored by this record</p>
*/
public record SerializableUUID(UUID uuid) implements ConfigurationSerializable {
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("id", uuid.toString());
return data;
}
/**
* Deserializes a serialized UUID
*
* @param data <p>The serialized data</p>
* @return <p>The deserialized UUID</p>
*/
@SuppressWarnings("unused")
public static SerializableUUID deserialize(Map<String, Object> data) {
String id = (String) data.getOrDefault("id", null);
if (id != null) {
return new SerializableUUID(UUID.fromString(id));
} else {
return null;
}
}
@Override
public boolean equals(Object object) {
if (object instanceof SerializableUUID) {
return this.uuid.equals(((SerializableUUID) object).uuid);
} else {
return false;
}
}
}

View File

@ -12,6 +12,9 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.util.Vector;
import java.util.HashSet;
import java.util.Set;
/**
* A listener for players moving inside a dropper arena
*/
@ -19,6 +22,11 @@ public class MoveListener implements Listener {
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
// Ignore if no actual movement is happening
if (event.getFrom().equals(event.getTo()) || event.getTo() == null) {
return;
}
Player player = event.getPlayer();
DropperArenaPlayerRegistry playerRegistry = Dropper.getInstance().getPlayerRegistry();
DropperArenaSession arenaSession = playerRegistry.getArenaSession(player.getUniqueId());
@ -26,29 +34,55 @@ public class MoveListener implements Listener {
return;
}
Block targetBlock = event.getTo().getBlock();
Material targetBlockType = targetBlock.getType();
// Hitting water is the trigger for winning
if (targetBlockType == Material.WATER) {
arenaSession.triggerWin();
// Prevent the player from flying upwards while in flight mode
if (event.getFrom().getY() < event.getTo().getY()) {
event.setCancelled(true);
return;
}
Location targetLocation = targetBlock.getLocation();
Material beneathPlayerType = targetLocation.getWorld().getBlockAt(targetLocation.add(0, -0.1, 0)).getType();
// 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
int depth = winBlockType == Material.WATER ? 0 : 1;
for (Block block : getBlocksBeneathLocation(event.getTo(), depth)) {
if (block.getType() == winBlockType) {
arenaSession.triggerWin();
return;
}
}
// If hitting something which is not air or water, it must be a solid block, and would end in a loss
if (!targetBlockType.isAir() || (beneathPlayerType != Material.WATER &&
!beneathPlayerType.isAir())) {
arenaSession.triggerLoss();
return;
// Check if the player is about to hit a non-air and non-liquid block
for (Block block : getBlocksBeneathLocation(event.getTo(), 1)) {
if (!block.getType().isAir() && block.getType() != Material.STRUCTURE_VOID &&
block.getType() != Material.WATER && block.getType() != Material.LAVA) {
arenaSession.triggerLoss();
return;
}
}
}
//Updates the player's velocity to the one set by the arena
updatePlayerVelocity(arenaSession);
}
/**
* Gets the blocks at the given location that will be affected by the player's hit-box
*
* @param location <p>The location to check</p>
* @return <p>The blocks beneath the player</p>
*/
private Set<Block> getBlocksBeneathLocation(Location location, double depth) {
Set<Block> blocksBeneath = new HashSet<>();
double halfPlayerWidth = 0.3;
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, -halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, -halfPlayerWidth).getBlock());
return blocksBeneath;
}
/**
* Updates the velocity of the player in the given session
*
@ -57,7 +91,7 @@ public class MoveListener implements Listener {
private void updatePlayerVelocity(DropperArenaSession session) {
Player player = session.getPlayer();
Vector playerVelocity = player.getVelocity();
double arenaVelocity = session.getArena().getPlayerVelocity();
double arenaVelocity = session.getArena().getPlayerVerticalVelocity();
Vector newVelocity = new Vector(playerVelocity.getX(), -arenaVelocity, playerVelocity.getZ());
player.setVelocity(newVelocity);
}

View File

@ -2,26 +2,61 @@ package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArenaSession;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
/**
* A listener for players leaving the server or the arena
*/
public class PlayerLeaveListener implements Listener {
private final Map<UUID, DropperArenaSession> leftSessions = new HashMap<>();
@EventHandler
public void onPlayerLeave(PlayerQuitEvent event) {
triggerQuit(event.getPlayer());
Player player = event.getPlayer();
DropperArenaSession arenaSession = getSession(player);
if (arenaSession == null) {
return;
}
Dropper.getInstance().getLogger().log(Level.WARNING, "Found player " + player.getUniqueId() +
" leaving in the middle of a session!");
leftSessions.put(player.getUniqueId(), arenaSession);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
UUID playerId = event.getPlayer().getUniqueId();
// Force the player to quit from the session once they re-join
if (leftSessions.containsKey(playerId)) {
Dropper.getInstance().getLogger().log(Level.WARNING, "Found un-exited dropper session!");
Bukkit.getScheduler().runTaskLater(Dropper.getInstance(), () -> {
leftSessions.get(playerId).triggerQuit();
Dropper.getInstance().getLogger().log(Level.WARNING, "Triggered a quit!");
leftSessions.remove(playerId);
}, 80);
}
}
@EventHandler
public void onPlayerTeleport(PlayerTeleportEvent event) {
if (event.getTo() == null || event.isCancelled()) {
return;
}
DropperArenaSession arenaSession = getSession(event.getPlayer());
if (arenaSession == null) {
return;
@ -31,24 +66,7 @@ public class PlayerLeaveListener implements Listener {
return;
}
triggerQuit(event.getPlayer());
}
/**
* Forces the given player to quit their current arena
*
* @param player <p>The player to trigger a quit for</p>
*/
private void triggerQuit(Player player) {
DropperArenaSession arenaSession = getSession(player);
if (arenaSession == null) {
return;
}
arenaSession.triggerQuit();
//TODO: It might not be possible to alter a leaving player's location here. It might be necessary to move them once
// they join again
}
/**

View File

@ -7,11 +7,45 @@ import org.jetbrains.annotations.NotNull;
*/
public enum ArenaStorageKey {
/**
* The key for an arena's name
*/
NAME("arenaName"),
/**
* The key for an arena's spawn location
*/
SPAWN_LOCATION("arenaSpawnLocation"),
/**
* The key for an arena's exit location
*/
EXIT_LOCATION("arenaExitLocation"),
PLAYER_VELOCITY("arenaPlayerVelocity"),
/**
* The key for a player in this arena's vertical velocity
*/
PLAYER_VERTICAL_VELOCITY("arenaPlayerVerticalVelocity"),
/**
* The key for a player in this arena's horizontal velocity
*/
PLAYER_HORIZONTAL_VELOCITY("arenaPlayerHorizontalVelocity"),
/**
* The key for this arena's stage
*/
STAGE("arenaStage"),
/**
* The key for the type of this arena's win block
*/
WIN_BLOCK_TYPE("winBlockType"),
/**
* The hey for this arena's records
*/
RECORDS("records"),
;
private final @NotNull String key;

View File

@ -3,8 +3,10 @@ package net.knarcraft.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry;
import net.knarcraft.dropper.container.SerializableMaterial;
import net.knarcraft.dropper.property.ArenaStorageKey;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
@ -45,8 +47,11 @@ public final class ArenaStorageHelper {
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_VELOCITY.getKey(), arena.getPlayerVelocity());
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()));
configSection.set(ArenaStorageKey.RECORDS.getKey(), arena.getRecordsRegistry());
}
//TODO: Save records belonging to the arena
configuration.save(arenaFile);
@ -94,16 +99,28 @@ public final class ArenaStorageHelper {
String arenaName = configurationSection.getString(ArenaStorageKey.NAME.getKey());
Location spawnLocation = (Location) configurationSection.get(ArenaStorageKey.SPAWN_LOCATION.getKey());
Location exitLocation = (Location) configurationSection.get(ArenaStorageKey.EXIT_LOCATION.getKey());
double playerVelocity = configurationSection.getDouble(ArenaStorageKey.PLAYER_VELOCITY.getKey());
double verticalVelocity = configurationSection.getDouble(ArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey());
double horizontalVelocity = 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());
DropperArenaRecordsRegistry recordsRegistry = (DropperArenaRecordsRegistry) configurationSection.get(
ArenaStorageKey.RECORDS.getKey());
if (arenaName == null || spawnLocation == null) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Could not load the arena at configuration " +
"section " + configurationSection.getName() + ". Please check the arenas storage file for issues.");
return null;
}
//TODO: Load records for this arena
return new DropperArena(arenaName, spawnLocation, exitLocation, playerVelocity, stage,
new DropperArenaRecordsRegistry());
if (winBlockType == null) {
winBlockType = new SerializableMaterial(Material.WATER);
}
if (recordsRegistry == null) {
recordsRegistry = new DropperArenaRecordsRegistry();
}
return new DropperArena(arenaName, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity, stage,
winBlockType.material(), recordsRegistry);
}
/**

View File

@ -0,0 +1,32 @@
package net.knarcraft.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A helper-class for common tab-completions
*/
public final class TabCompleteHelper {
private TabCompleteHelper() {
}
/**
* Gets the names of all current arenas
*
* @return <p>All arena names</p>
*/
public static @NotNull List<String> getArenas() {
List<String> arenaNames = new ArrayList<>();
for (DropperArena dropperArena : Dropper.getInstance().getArenaHandler().getArenas()) {
arenaNames.add(dropperArena.getArenaName());
}
return arenaNames;
}
}

View File

@ -5,11 +5,21 @@ api-version: 1.19
description: A plugin for dropper mini-games
commands:
dropperreload:
aliases:
- dreload
permission: dropper.admin
usage: /<command>
description: Reloads all data from disk
dropperlist:
aliases:
- dlist
permission: dropper.join
usage: /<command>
description: Used to list all current dropper arenas
dropperjoin:
aliases:
- djoin
permission: dropper.join
usage: |
/<command> <arena> [mode]
@ -18,18 +28,26 @@ commands:
time = A shortest-time competitive game-mode
description: Used to join a dropper arena
dropperleave:
aliases:
- dleave
permission: dropper.join
usage: /<command>
description: Used to leave the current dropper arena
droppercreate:
aliases:
- dcreate
permission: dropper.create
usage: /<command> (Details not finalized)
description: Used to create a new dropper arena
dropperedit:
aliases:
- dedit
permission: dropper.edit
usage: /<command> (Details not finalized)
description: Used to edit an existing dropper arena
dropperremove:
aliases:
- dremove
permission: dropper.remove
usage: /<command> <arena>
description: Used to remove an existing dropper arena