Implements #13 and #14

Removes deaths and time as separate game-modes, and instead always tracks records.
Adds two proper game-modes. One inverts the player's controls, and the other randomly inverts the player's controls every 7 seconds.
Saves cleared status and records for each game-mode separately
Only allows 0-1 for the horizontal velocity
Requires arenas in arena groups to be cleared in sequence on all game-modes, not just the default one.
This commit is contained in:
Kristian Knarvik 2023-03-30 14:59:44 +02:00
parent 572bb980c1
commit f9008ca050
14 changed files with 258 additions and 108 deletions

View File

@ -36,10 +36,10 @@ This command is used for joining a dropper arena.
`/droppejoin <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
@ -61,5 +61,5 @@ These are all the options that can be changed for an arena
| 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 -1 and 1, and cannot be 0. 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. |

View File

@ -22,6 +22,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;
@ -89,6 +90,7 @@ public final class Dropper extends JavaPlugin {
ConfigurationSerialization.registerClass(SerializableUUID.class);
ConfigurationSerialization.registerClass(DropperArenaData.class);
ConfigurationSerialization.registerClass(DropperArenaGroup.class);
ConfigurationSerialization.registerClass(ArenaGameMode.class);
}
@Override

View File

@ -1,6 +1,7 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.util.ArenaStorageHelper;
import org.bukkit.Location;
import org.bukkit.Material;
@ -8,7 +9,8 @@ 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;
/**
@ -106,8 +108,13 @@ public class DropperArena {
this.exitLocation = null;
this.playerVerticalVelocity = 3.92;
this.playerHorizontalVelocity = 1;
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;
}

View File

@ -2,6 +2,7 @@ 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;
@ -16,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<UUID> 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<UUID> 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 hasNotCompleted(@NotNull Player player) {
return !this.playersCompleted.contains(player.getUniqueId());
public boolean hasNotCompleted(@NotNull ArenaGameMode arenaGameMode, @NotNull Player player) {
return !this.playersCompleted.getOrDefault(arenaGameMode, new HashSet<>()).contains(player.getUniqueId());
}
/**
@ -51,8 +55,13 @@ public record DropperArenaData(@NotNull UUID arenaId, @NotNull DropperArenaRecor
*
* @param player <p>The player that completed this data's arena</p>
*/
public boolean addCompleted(@NotNull Player player) {
boolean added = this.playersCompleted.add(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);
@ -65,13 +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("recordsRegistry", this.recordRegistries);
Set<SerializableUUID> playersCompleted = new HashSet<>();
for (UUID playerCompleted : this.playersCompleted) {
playersCompleted.add(new SerializableUUID(playerCompleted));
// 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", playersCompleted);
data.put("playersCompleted", serializablePlayersCompleted);
return data;
}
@ -84,13 +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> playersCompletedData = (Set<SerializableUUID>) data.get("playersCompleted");
Set<UUID> playersCompleted = new HashSet<>();
for (SerializableUUID completedId : playersCompletedData) {
playersCompleted.add(completedId.uuid());
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, playersCompleted);
return new DropperArenaData(serializableUUID.uuid(), recordsRegistry, allPlayersCompleted);
}
}

View File

@ -2,6 +2,7 @@ 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.ArenaStorageHelper;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.entity.Player;
@ -117,12 +118,13 @@ public class DropperArenaGroup implements ConfigurationSerializable {
}
/**
* Checks whether the given player has beaten all arenas in this group
* Checks whether the given player has beaten all arenas in this group on the given game-mode
*
* @param player <p>The player to check</p>
* @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(Player player) {
public boolean hasBeatenAll(ArenaGameMode gameMode, Player player) {
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
for (UUID anArenaId : this.getArenas()) {
DropperArena dropperArena = arenaHandler.getArena(anArenaId);
@ -133,7 +135,7 @@ public class DropperArenaGroup implements ConfigurationSerializable {
continue;
}
if (dropperArena.getData().hasNotCompleted(player)) {
if (dropperArena.getData().hasNotCompleted(gameMode, player)) {
return false;
}
}
@ -141,14 +143,15 @@ public class DropperArenaGroup implements ConfigurationSerializable {
}
/**
* Gets whether the given player can play the given arena part of this group
* Gets whether the given player can play the given arena part of this group, on the given game-mode
*
* @param player <p>The player to check</p>
* @param arenaId <p>The id of the arena in this group to check</p>
* @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(Player player, UUID arenaId) throws IllegalArgumentException {
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!");
}
@ -170,7 +173,7 @@ public class DropperArenaGroup implements ConfigurationSerializable {
}
// This is a lower-numbered arena the player has yet to complete
if (dropperArena.getData().hasNotCompleted(player)) {
if (dropperArena.getData().hasNotCompleted(gameMode, player)) {
return false;
}
}

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
*
@ -62,7 +71,7 @@ public class DropperArenaSession {
registerRecord();
// Mark the arena as cleared
if (this.arena.getData().addCompleted(this.player)) {
if (this.arena.getData().addCompleted(this.gameMode, this.player)) {
this.player.sendMessage("You cleared the arena!");
}
this.player.sendMessage("You won!");
@ -101,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,13 +19,14 @@ 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().clone();
this.originalFlySpeed = player.getFlySpeed();
@ -32,6 +35,7 @@ public class PlayerEntryState {
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

@ -120,7 +120,7 @@ public class EditArenaCommand implements CommandExecutor {
}
// If outside bonds, choose the most extreme value
return Math.min(Math.max(-1, velocity), 1);
return Math.min(Math.max(0.1f, velocity), 1);
}
/**

View File

@ -76,9 +76,10 @@ public class JoinArenaCommand implements CommandExecutor {
return false;
}
// Make sure the player has beaten the arena once before playing a challenge mode
if (gameMode != ArenaGameMode.DEFAULT && specifiedArena.getData().hasNotCompleted(player)) {
player.sendMessage("You must complete this arena in normal mode before starting a challenge!");
// 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;
}
@ -112,18 +113,20 @@ public class JoinArenaCommand implements CommandExecutor {
*/
private boolean doGroupChecks(@NotNull DropperArena dropperArena, @NotNull DropperArenaGroup arenaGroup,
@NotNull ArenaGameMode arenaGameMode, @NotNull Player player) {
if (arenaGameMode == ArenaGameMode.DEFAULT) {
if (!arenaGroup.canPlay(player, dropperArena.getArenaId())) {
player.sendMessage("You have not yet beaten the previous arena!");
return false;
}
} else {
if (arenaGroup.hasBeatenAll(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

@ -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

@ -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

@ -7,6 +7,7 @@ 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;
@ -164,7 +165,12 @@ 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,

View File

@ -24,8 +24,8 @@ commands:
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
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:
aliases: