Adds various necessary code

Moves code for keeping track of players to DropperArenaPlayerRegistry
Adds a DropperArenaRecordsRegistry for keeping track of records
Stores playing players as DropperArenaSession to be able to store more metadata
Adds an enum for different arena game-modes
Adds a record result enum for defining which kind of record a player achieved (personal or global)
Adds a helper-class for teleporting a player to and from arenas
This commit is contained in:
Kristian Knarvik 2023-03-23 13:00:05 +01:00
parent 724de147fd
commit 62450e8764
14 changed files with 431 additions and 58 deletions

View File

@ -1,13 +1,18 @@
package net.knarcraft.dropper;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import org.bukkit.plugin.java.JavaPlugin;
/**
* The dropper plugin's main class
*/
@SuppressWarnings("unused")
public final class Dropper extends JavaPlugin {
private static Dropper instance;
private DropperArenaHandler arenaHandler;
private DropperArenaPlayerRegistry playerRegistry;
/**
* Gets an instance of this plugin
@ -18,10 +23,29 @@ public final class Dropper extends JavaPlugin {
return instance;
}
/**
* Gets the arena handler for this instance
*
* @return <p>A dropper arena handler</p>
*/
public DropperArenaHandler getArenaHandler() {
return this.arenaHandler;
}
/**
* Gets the arena player registry for this instance
*
* @return <p>A dropper arena player registry</p>
*/
public DropperArenaPlayerRegistry getPlayerRegistry() {
return this.playerRegistry;
}
@Override
public void onEnable() {
// Plugin startup logic
instance = this;
this.playerRegistry = new DropperArenaPlayerRegistry();
this.arenaHandler = new DropperArenaHandler();
this.arenaHandler.loadArenas();
@ -44,6 +68,8 @@ public final class Dropper extends JavaPlugin {
//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: Set player.setAllowFlight to true while in the arena to avoid flight blocking for high velocities
//TODO: Register event listeners
//TODO: Register commands

View File

@ -35,22 +35,32 @@ public class DropperArena {
*/
private final @Nullable Integer stage;
/**
* The registry used to save this arena's records
*/
private final @NotNull DropperArenaRecordsRegistry recordsRegistry;
//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 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>
*/
public DropperArena(@NotNull String arenaName, @NotNull Location spawnLocation, @Nullable Location exitLocation,
double playerVelocity, @Nullable Integer stage) {
double playerVelocity, @Nullable Integer stage, @NotNull DropperArenaRecordsRegistry recordsRegistry) {
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = exitLocation;
this.playerVelocity = playerVelocity;
this.stage = stage;
this.recordsRegistry = recordsRegistry;
}
/**
@ -68,6 +78,7 @@ public class DropperArena {
this.exitLocation = null;
this.playerVelocity = 1;
this.stage = null;
this.recordsRegistry = new DropperArenaRecordsRegistry();
}
/**

View File

@ -2,16 +2,12 @@ package net.knarcraft.dropper.arena;
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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
/**
@ -22,46 +18,6 @@ public class DropperArenaHandler {
private static final File arenaFile = new File(Dropper.getInstance().getDataFolder(), "arenas.yml");
private List<DropperArena> arenas = new ArrayList<>();
private final Map<Player, DropperArena> arenaPlayers = new HashMap<>();
/**
* Registers that the given player has started playing in the given dropper arena
*
* @param player <p>The player that started playing</p>
* @param arena <p>The arena the player is playing in</p>
*/
public void registerPlayer(@NotNull Player player, @NotNull DropperArena arena) {
this.arenaPlayers.put(player, arena);
}
/**
* Removes this player from players currently playing
*
* @param player <p>The player to remove</p>
*/
public void removePlayer(@NotNull Player player) {
this.arenaPlayers.remove(player);
}
/**
* Gets whether the given player is currently playing in an arena
*
* @param player <p>The player to check</p>
* @return <p>True if the player is currently in an arena</p>
*/
public boolean isInArena(@NotNull Player player) {
return getArena(player) != null;
}
/**
* Gets the arena the given player is currently playing in
*
* @param player <p>The player to get arena for</p>
* @return <p>The player's active arena, or null if not currently playing</p>
*/
public @Nullable DropperArena getArena(@NotNull Player player) {
return this.arenaPlayers.getOrDefault(player, null);
}
/**
* Adds a new arena

View File

@ -0,0 +1,56 @@
package net.knarcraft.dropper.arena;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* A registry to keep track of which players are playing in which arenas
*/
public class DropperArenaPlayerRegistry {
private final Map<Player, DropperArenaSession> arenaPlayers = new HashMap<>();
/**
* Registers that the given player has started playing the given dropper arena session
*
* @param player <p>The player that started playing</p>
* @param arena <p>The arena session to register</p>
*/
public void registerPlayer(@NotNull Player player, @NotNull DropperArenaSession arena) {
this.arenaPlayers.put(player, arena);
}
/**
* Removes this player from players currently playing
*
* @param player <p>The player to remove</p>
*/
public void removePlayer(@NotNull Player player) {
this.arenaPlayers.remove(player);
}
/**
* Gets whether the given player is currently playing in an arena
*
* @param player <p>The player to check</p>
* @return <p>True if the player is currently in an arena</p>
*/
public boolean isInArena(@NotNull Player player) {
return getArenaSession(player) != null;
}
/**
* Gets the player's active dropper arena session
*
* @param player <p>The player to get arena for</p>
* @return <p>The player's active arena session, or null if not currently playing</p>
*/
public @Nullable DropperArenaSession getArenaSession(@NotNull Player player) {
return this.arenaPlayers.getOrDefault(player, null);
}
}

View File

@ -0,0 +1,109 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.property.RecordResult;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
/**
* A registry keeping track of all records
*/
public class DropperArenaRecordsRegistry {
private final Map<Player, Integer> leastDeaths;
private final Map<Player, Long> shortestTimeMilliSeconds;
/**
* Instantiates a new empty records registry
*/
public DropperArenaRecordsRegistry() {
this.leastDeaths = new HashMap<>();
this.shortestTimeMilliSeconds = new HashMap<>();
}
/**
* Instantiates a new records registry
*
* @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) {
this.leastDeaths = new HashMap<>(leastDeaths);
this.shortestTimeMilliSeconds = new HashMap<>(shortestTimeMilliSeconds);
}
/**
* Gets all existing death records
*
* @return <p>Existing death records</p>
*/
public Map<Player, Integer> getLeastDeathsRecords() {
return new HashMap<>(this.leastDeaths);
}
/**
* Gets all existing time records
*
* @return <p>Existing time records</p>
*/
public Map<Player, 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>
* @return <p>The result explaining what type of record was achieved</p>
*/
public @NotNull RecordResult registerDeathRecord(@NotNull Player player, int deaths) {
RecordResult result;
Stream<Map.Entry<Player, Integer>> records = leastDeaths.entrySet().stream();
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)) {
//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);
} else {
result = RecordResult.NONE;
}
return result;
}
/**
* Registers a new time-record
*
* @param player <p>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) {
RecordResult result;
Stream<Map.Entry<Player, Long>> records = shortestTimeMilliSeconds.entrySet().stream();
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)) {
//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);
} else {
result = RecordResult.NONE;
}
return result;
}
}

View File

@ -0,0 +1,52 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.property.ArenaGameMode;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class DropperArenaSession {
private final DropperArena arena;
private final Player player;
private final ArenaGameMode gameMode;
private final int deaths;
private final long startTime;
/**
* Instantiates a new dropper arena session
*
* @param dropperArena <p>The arena that's being played in</p>
* @param player <p>The player playing the arena</p>
* @param gameMode <p>The game-mode</p>
*/
public DropperArenaSession(@NotNull DropperArena dropperArena, @NotNull Player player,
@NotNull ArenaGameMode gameMode) {
this.arena = dropperArena;
this.player = player;
this.gameMode = gameMode;
this.deaths = 0;
this.startTime = System.currentTimeMillis();
}
public void triggerWin() {
//TODO: Kick the player from the arena
//TODO: Register the player's record, if applicable, and announce the result
//TODO: Give reward?
//TODO: If a staged arena, register the stage as cleared
//TODO: Teleport the player out of the dropper arena
}
public void triggerLoss() {
switch (gameMode) {
case DEFAULT:
//TODO: Kick the player, and teleport the player away
break;
case LEAST_TIME:
//TODO: Teleport the player back to the top
break;
case LEAST_DEATHS:
//TODO: Add 1 to the death count, and teleport the player back to the top
}
}
}

View File

@ -3,16 +3,24 @@ package net.knarcraft.dropper.command;
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;
/**
* The command used to join a dropper arena
*/
public class JoinArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] strings) {
if (!(commandSender instanceof Player)) {
commandSender.sendMessage("This command must be used by a player");
return false;
}
//TODO: Implement command behavior
//TODO: Remember to check if the player is already in an arena first!
return false;
return true;
}
}

View File

@ -1,22 +1,36 @@
package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
/**
* A listener for checking if a player takes damage within a dropper arena
*/
public class DamageListener implements Listener {
@EventHandler
public void onPlayerDamage(EntityDamageEvent event) {
// Only player damage matters
if (event.getEntityType() != EntityType.PLAYER) {
return;
}
//TODO: Check if the player is in the arena (return if not)
//TODO: Cancel the event to prevent the player from taking damage or dying
Player player = (Player) event.getEntity();
// We don't care about damage outside arenas
if (!Dropper.getInstance().getPlayerRegistry().isInArena(player)) {
return;
}
event.setCancelled(true);
//TODO: Kick the player from the arena
//TODO: Teleport the player to the location they entered the arena from, or to the spawn
//TODO: Do whatever else should be done for losing players (sending a message?)
}
}

View File

@ -1,17 +1,40 @@
package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
/**
* A listener for players moving inside a dropper arena
*/
public class MoveListener implements Listener {
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
//TODO: Return if player is not in an arena
//TODO: If the player is moving to a block of water, register the win, and teleport the player out
//TODO: If the player is about to hit a non-water and non-air block (within a margin of about 1/16 of a block
// in the y-direction), treat that the same as @see{DamageListener#onPlayerDamage}
if (!Dropper.getInstance().getPlayerRegistry().isInArena(event.getPlayer())) {
return;
}
Block targetBlock = event.getTo().getBlock();
// Hitting water is the trigger for winning
if (targetBlock.getType() == Material.WATER) {
//TODO: Register the win, and teleport the player out
return;
}
Location targetLocation = targetBlock.getLocation();
Block beneathPlayer = targetLocation.getWorld().getBlockAt(targetLocation.add(0, -0.1, 0));
// If hitting something which is not air or water, it must be a solid block, and would end in a loss
if (!targetBlock.getType().isAir() || (beneathPlayer.getType() != Material.WATER &&
!beneathPlayer.getType().isAir())) {
//TODO: This that the same as @see{DamageListener#onPlayerDamage}
}
}
}

View File

@ -1,5 +1,6 @@
package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
@ -9,6 +10,9 @@ public class PlayerLeaveListener implements Listener {
@EventHandler
public void onPlayerLeave(PlayerQuitEvent event) {
if (!Dropper.getInstance().getPlayerRegistry().isInArena(event.getPlayer())) {
return;
}
//TODO: If in an arena, kick the player.
//TODO: Teleport the player away from the arena. It might only be possible to teleport the player when they join
// again.
@ -16,8 +20,12 @@ public class PlayerLeaveListener implements Listener {
@EventHandler
public void onPlayerTeleport(PlayerTeleportEvent event) {
if (!Dropper.getInstance().getPlayerRegistry().isInArena(event.getPlayer())) {
return;
}
//TODO: Treat this the same as onPlayerLeave if the player is in an arena. If the player doesn't change worlds,
// it should be safe to immediately teleport the player to the arena's exit.
//TODO: Because of this, make sure to remove the player from the arena before teleporting the player out
}
}

View File

@ -0,0 +1,24 @@
package net.knarcraft.dropper.property;
/**
* A representation of possible arena game-modes
*/
public enum ArenaGameMode {
/**
* The default game-mode. Failing once throws the player out.
* //TODO: Verify if we want the default game-mode to lock the player in the arena until they beat it
*/
DEFAULT,
/**
* The least-deaths game-mode. Player plays until they manage to win. The number of deaths is recorded.
*/
LEAST_DEATHS,
/**
* The least-time game-mode. Player plays until they manage to win. The total time of the session is recorded.
*/
LEAST_TIME,
}

View File

@ -0,0 +1,23 @@
package net.knarcraft.dropper.property;
/**
* A representation of all possible record results
*/
public enum RecordResult {
/**
* No record was achieved
*/
NONE,
/**
* A personal bes was achieved
*/
PERSONAL_BEST,
/**
* A world record was achieved
*/
WORLD_RECORD
}

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.arena.DropperArenaRecordsRegistry;
import net.knarcraft.dropper.property.ArenaStorageKey;
import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection;
@ -22,6 +23,10 @@ public final class ArenaStorageHelper {
private final static String arenasConfigurationSection = "arenas";
private ArenaStorageHelper() {
}
/**
* Saves the given arenas to the given file
*
@ -43,6 +48,7 @@ public final class ArenaStorageHelper {
configSection.set(ArenaStorageKey.PLAYER_VELOCITY.getKey(), arena.getPlayerVelocity());
configSection.set(ArenaStorageKey.STAGE.getKey(), arena.getStage());
}
//TODO: Save records belonging to the arena
configuration.save(arenaFile);
}
@ -95,7 +101,9 @@ public final class ArenaStorageHelper {
"section " + configurationSection.getName() + ". Please check the arenas storage file for issues.");
return null;
}
return new DropperArena(arenaName, spawnLocation, exitLocation, playerVelocity, stage);
//TODO: Load records for this arena
return new DropperArena(arenaName, spawnLocation, exitLocation, playerVelocity, stage,
new DropperArenaRecordsRegistry());
}
/**

View File

@ -0,0 +1,55 @@
package net.knarcraft.dropper.util;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
/**
* A helper class for teleporting players
*/
public final class PlayerTeleporter {
private PlayerTeleporter() {
}
/**
* Teleports the given player to the given location
*
* <p>Forcing teleport should only be used inside an arena, to prevent the player from becoming stuck.</p>
*
* @param player <p>The player about to teleport</p>
* @param location <p>The location the player should be teleported to</p>
* @param force <p>Whether to force a player teleport, even in a vehicle or a passenger</p>
* @return <p>True if the player was successfully teleported</p>
*/
public static boolean teleportPlayer(Player player, Location location, boolean force) {
if (!player.getPassengers().isEmpty()) {
if (force) {
for (Entity passenger : player.getPassengers()) {
passenger.eject();
passenger.teleport(location);
}
} else {
player.sendMessage("You cannot be teleported with a passenger!");
return false;
}
}
if (player.isInsideVehicle()) {
if (force && player.getVehicle() != null) {
Entity vehicle = player.getVehicle();
player.eject();
vehicle.teleport(location);
} else {
player.sendMessage("You cannot be teleported while in a vehicle");
return false;
}
}
//Stop the player velocity to prevent unevenness between players
player.setVelocity(new Vector(0, 0, 0));
player.teleport(location);
return true;
}
}