diff --git a/OG_GUIDE.md b/OG_GUIDE.md new file mode 100644 index 0000000..7850681 --- /dev/null +++ b/OG_GUIDE.md @@ -0,0 +1,117 @@ +# Tumble + +## Guide for original worlds + +In this guide, I'll go over how to set up the Tumble plugin with the original game worlds from the Legacy Console Editions. + +## Steps + +1. Download this plugin and [Multiverse-Core](https://www.spigotmc.org/resources/multiverse-core.390/). Place them in your plugins directory +2. Download the worlds and unzip them into your server's worlds directory. + - [Lobby](https://www.theminecraftarchitect.com/mini-game-maps/2017-mini-game-lobby) (Rename folder to 'lobby' after unzipping) + - [Normal Arena](https://publicfiles.sowgro.net/console-minigame-maps/java/tumble/) + - [Festive Arena](https://publicfiles.sowgro.net/console-minigame-maps/java/tumble/) + - [Halloween Arena](https://publicfiles.sowgro.net/console-minigame-maps/java/tumble/) + - [Birthday Arena](https://publicfiles.sowgro.net/console-minigame-maps/java/tumble/) + + *Tip: set a specific directory to store your worlds in with the `world-container` setting in `bukkit.yml`.* + +3. Set `level-name` in server.properties to `lobby` +4. Take note of the names of the world folders, we will need these in step six. +5. Start and join your server. +6. Import your arena worlds. This can be done with the multiverse command `/mv import normal` + +7. Paste the following arena config below into `plugins/Tumble/arenas.yml`: + ```yaml + arenas: + basic: + kill-at-y: 24 + game-spawn: + x: 0.5 + y: 60.0 + z: 0.5 + world: basic + lobby: + x: -341.5 + y: 58 + z: -340.5 + world: lobby + winner-lobby: + x: -362.5 + y: 76 + z: -340.5 + world: lobby + birthday: + kill-at-y: 27 + game-spawn: + x: 0.5 + y: 60 + z: 0.5 + world: birthday + lobby: + x: -341.5 + y: 58 + z: -340.5 + world: lobby + winner-lobby: + x: -362.5 + y: 76 + z: -340.5 + world: lobby + festive: + kill-at-y: 20 + game-spawn: + x: 0.5 + y: 60 + z: 0.5 + world: festive + lobby: + x: -341.5 + y: 58 + z: -340.5 + world: lobby + winner-lobby: + x: -362.5 + y: 76 + z: -340.5 + world: lobby + halloween: + kill-at-y: 23 + game-spawn: + x: 0.5 + y: 60 + z: -0.5 + world: halloween + lobby: + x: -341.5 + y: 58 + z: -340.5 + world: lobby + winner-lobby: + x: -362.5 + y: 76 + z: -340.5 + world: lobby + ``` +8. Reload the plugin with `/tumble reload`. + +9. Join the game using `/tumble join basic mixed` +(swap the arena and game type for whichever one you want to play). + +You're done! Happy playing! + +## Recommended plugins + +These plugins, while not required, can help to provide a more thorough minigame experience. + +- [WorldGuard](https://dev.bukkit.org/projects/worldguard) and [CyberWorldReset](https://www.spigotmc.org/resources/cyberworldreset-standard-%E2%9C%A8-regenerate-worlds-scheduled-resets-lag-optimized%E3%80%8C1-8-1-19%E3%80%8D.96834/) + - Protect players from breaking blocks in the lobby and reset any redstone they activated. + +- [ViaVersion](https://www.spigotmc.org/resources/viaversion.19254/) and [ViaBackwards](https://www.spigotmc.org/resources/viabackwards.27448/) + - Allow older and newer players to connect to your server. + +- [Geyser](https://geysermc.org/download#geyser) and [Floodgate](https://geysermc.org/download#floodgate) + - Allow Bedrock clients to connect to your server. + +- [ProtectEnvironment](https://www.spigotmc.org/resources/protectenvironment.82736/) + - Stop water and lava flow (useful for Halloween map). diff --git a/README.md b/README.md index a2b349b..05085d6 100644 --- a/README.md +++ b/README.md @@ -6,76 +6,114 @@ Tumble is a Spigot/Paper plugin that aims to recreate the Tumble minigame from t ## What *is* Tumble? -If you've never heard of it, [Tumble](https://minecraft-archive.fandom.com/wiki/Tumble_Mode) is a twist on the classic Minecraft minigame of spleef, where the objective is to break the blocks under your opponents. But in Tumble, you play on randomly generated layers of blocks, using shovels, snowballs, or both to try and eliminate your opponents. +If you've never heard of it, [Tumble](https://minecraft.wiki/w/tumble) is a twist on the classic Minecraft minigame of spleef, where the objective is to break the blocks under your opponents. +But in Tumble, you play on randomly generated layers of blocks, using shovels, snowballs, or both to try and eliminate your opponents. ## Features -- Choose from three different game modes present in the original game--shovels, snowballs, and mixed -- Four types of random layer generation -- 15 unique, themed layer varieties +- Choose from three different game modes present in the original game: shovels, snowballs, and mixed +- Four types of random layer generation +- 16 unique, themed layer varieties, and the ability to add your own - Quick and easy setup and use -- Support for 2-8 players -- Highly customizable -- Open-source codebase +- Support for 2-8 players +- Multiple arenas and concurrent games +- Highly customizable, heavily configurable +- Open-source codebase ## Setup -1. Simply [download](https://github.com/MylesAndMore/Tumble/releases) the plugin's JAR file and place it in your server's plugins directory. +1. [Download](https://github.com/MylesAndMore/Tumble/releases) the plugin's JAR file and place it in your server's plugins directory. +2. If you'd like to have your lobby and arena(s) in separate worlds... + - Place the worlds for your lobby and arena(s) in your server's worlds directory. + - Import your worlds using a plugin like Multiverse ```/mv import myWorld normal```. + - If you would like an experience similar to the original game, see [my guide](OG_GUIDE.md) for using the original worlds. +3. Start your server. +4. Create your first arena `/tumble create myArena`. +5. Set the spawn point of the arena `/tumble setgamespawn myArena`. + - **Note**: The layers will generate relative to this location. Ensure that the area is clear, 20 blocks in each direction. - - *Note: Multiverse is also required for the plugin to run, you may download it [here](https://www.spigotmc.org/resources/multiverse-core.390/).* +6. You're done! You can now join the game ```/tumble join myArena mixed```. -2. Make sure that you have at least two worlds in your world directory! One is for your lobby world, and the other is for your game arena. +Scroll down for more options to configure your game. - - If you would like an experience similar to the original game, see [my guide](https://github.com/MylesAndMore/tumble/blob/main/og-guide.md) for using the original worlds. +## Commands / Permissions + +| Command | Description | Permission | +|---------------------------------------|-------------------------------------------------------------------------------------|-------------------------| +| `/tumble join [gameType]` | Join a Tumble match. Can infer game type if a game is already started in the arena. | `tumble.join` | +| `/tumble leave` | Leave a Tumble match. | `tumble.leave` | +| `/tumble forcestart [arenaName]` | Force start a Tumble match. Can infer arena if you are in one. | `tumble.forcestart` | +| `/tumble forcestop [arenaName]` | Force stop a Tumble match. Can infer arena if you are in one. | `tumble.forcestop` | +| `/tumble reload` | Reload the plugin's configuration. | `tumble.reload` | +| `/tumble create ` | Create a new arena. | `tumble.create` | +| `/tumble remove ` | Remove an arena. | `tumble.remove` | +| `/tumble setgamespawn ` | Set the arena's game spawn to your current position. | `tumble.setgamespawn` | +| `/tumble setkillylevel ` | Set the arena's Y-level to kill players at to current Y coordinate. | `tumble.setkillylevel` | +| `/tumble setlobby ` | Set the arena's lobby to current location. | `tumble.setlobby` | +| `/tumble setwaitarea ` | Set the arena's wait area to the current location. | `tumble.setwaitarea` | +| `/tumble setwinnerlobby ` | Set the arena's winner lobby to the current location. | `tumble.setwinnerlobby` | + +Note that the `/tmbl` command can be used as a shorter alias to `/tumble`. -3. Start your server. The plugin will generate a couple of warnings, these are normal. -4. Ensure that you have imported your worlds into Multiverse. This can be done with the command ```/mv import normal```. -5. Now you need to tell Tumble which world is your lobby and which world is your game arena. You can do this with ```/tumble:link lobby``` and ```/tumble:link game``` respectively. -6. **VERY IMPORTANT:** The plugin will teleport players to the world spawn point of each world, and generate the game's blocks around the spawn point of the game world. Ensure that your spawn points are clear of any obstructions, and that a 20x20x20 cube is cleared out from the spawn of whatever game world you are using. **Any blocks in this area will be destroyed when the game begins.** -7. You're done! You can now start games with the command ```/tumble:start```. +## Configuration +Configuration for this plugin is stored in three files: -Scroll down for more options to configure your game. +### settings.yml +Stores general settings. -## Commands/Permissions - -- ```/tumble:reload``` - - - *Description:* Reloads the plugin's configuration. - - *Usage:* ```/tumble:reload``` - - *Permission:* ```tumble.reload``` -- ```/tumble:link``` - - *Description:* Links a world on the server as a lobby or game world. - - *Usage:* ```/tumble:link (lobby|game)``` - - *Permission:* ```tumble.link``` -- ```/tumble:start``` - - *Description:* Force starts a Tumble match (with an optional game type). - - *Usage:* ```/tumble:start [game-type]``` - - *Permission:* ```tumble.start``` -- ```/tumble:winlocation``` - - *Description:* Sets the location to teleport the winning player of a game. Uses the player's location if no arguments are specified. - - *Usage:* ```/tumble:winlocation [x] [y] [z]``` - - *Permission:* ```tumble.winlocation``` -- ```/tumble:autostart``` - - *Description:* Configures the auto start functions of Tumble. - - *Usage:* ```/tumble:autostart [enable|disable]``` - - *Permission:* ```tumble.autostart``` -- *Permission:* ```tumble.update``` - - Players with this permission will receive a notification upon joining if Tumble is out of date. +| Option | Type | Description | Default value | +|----------------------------|---------|--------------------------------------------------------------------------------|---------------| +| `hide-join-leave-messages` | Boolean | Hides player join and leave messages in public chat. | `false` | +| `hide-death-messages` | Boolean | Hides player death messages in public chat. | `false` | +| `wait-duration` | Integer | Duration (in seconds) to wait for more players to join a game before starting. | `15` | -## Configuration +### arenas.yml +Stores data about each arena. +Arenas may be added and removed as you wish, either via the commands detailed above or by editing the `arenas.yml` file directly. + +Each arena can contain the following locations: + +| Location | Description | +|--------------------------|-------------------------------------------------------------------------------------| +| `game-spawn` **Required* | The location where players will be teleported, and the layers will generate around. | +| `wait-area` | The location where players will be teleported to before the game begins. | +| `lobby` | The location where players will be teleported to after the game ends. | +| `winner-lobby` | The location where the winner will be teleported after the game ends. | + +Locations are stored using the following format: +```yaml +location: + x: 0.5 + y: 100 + z: 0.5 + world: worldName +``` +If a location is not specified, players will not be teleported by the plugin. + +Each arena can also contain the following option: + +| Option | Type | Description | +|-------------|---------|------------------------------------------------------------------| +| `kill-at-y` | Integer | When a player falls below this Y-level, they will be eliminated. | + +### language.yml +Most of this plugin's messages are configurable through this file (excluding some console errors). + +All plugin chat messages will have the `prefix` prepended to them. + +Colors can be added using alternate color codes; for example, `&cRed Text` will appear red in-game. + +### layers.yml +Stores data about the layers that will be generated during gameplay. -- ```gameMode``` - - Customize the default game mode of Tumble. - - Acceptable options include: shovels, snowballs, mixed - - *Default:* ```mixed``` +Each layer contains a weight and a list of materials (blocks). -- ```hideJoinLeaveMessages``` - - Hides join/leave messages in public chat. - - *Default:* ```false``` +| Option | Type | Description | +|-------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `weight` | Integer | A weight to influence how often the layer is randomly chosen. Default: 1 | +| `materials` **Required* | List of Materials | The palette of blocks that the layer will be composed of. Use the block names [listed here](https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html). Optionally, a weight can be added after the block name like so: `STONE 5`. |` -- ```permissionMessage``` - - Customize the message that displays when the player does not have permission to execute a command from this plugin. ## Issues & Feedback -Feel free to report any bugs, leave feedback, ask questions, or submit ideas for new features on our [GitHub issues page](https://github.com/MylesAndMore/tumble/issues/new)! +Feel free to report any bugs, leave feedback, ask questions, or submit ideas for new features on the Tumble [GitHub issues page](https://github.com/MylesAndMore/tumble/issues/new)! diff --git a/build.gradle b/build.gradle index 8791ad3..41f2c92 100644 --- a/build.gradle +++ b/build.gradle @@ -5,23 +5,20 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(16) } } repositories { - mavenCentral() // Use Maven Central for resolving dependencies. + mavenCentral() // Use Maven Central for resolving dependencies maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } - maven { url "https://repo.onarandombox.com/content/groups/public/" } - maven { url "https://repo.jeff-media.com/public/"} } dependencies { - compileOnly('org.spigotmc:spigot-api:1.19.2-R0.1-SNAPSHOT') - compileOnly('com.onarandombox.multiversecore:Multiverse-Core:4.3.1') + compileOnly('org.jetbrains:annotations:24.1.0') + compileOnly('org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT') implementation('org.bstats:bstats-bukkit:3.0.2') - implementation('com.jeff_media:SpigotUpdateChecker:3.0.3') } // Disable generation of the normal JAR to reduce confusion and only generate the correctly shadowed JAR including bStats @@ -30,7 +27,6 @@ jar.finalizedBy(shadowJar) shadowJar { archiveBaseName.set('Tumble') archiveClassifier.set('') - archiveVersion.set('1.0.4') + archiveVersion.set('2.0.0') relocate 'org.bstats', 'com.MylesAndMore.bstats' - relocate 'com.jeff_media.updatechecker', 'com.MylesAndMore.updatechecker' } diff --git a/og-guide.md b/og-guide.md deleted file mode 100644 index 9c613a8..0000000 --- a/og-guide.md +++ /dev/null @@ -1,33 +0,0 @@ -# Tumble - -## Guide for original worlds - -In this guide, I'll go over how to set up the Tumble plugin with the original game worlds from the Legacy Console Editions. - -## Steps - -1. Download the worlds and unzip them into your server's main/root directory. **Ensure you download the Java and not the Bedrock versions**! -A huge thanks to *Catmanjoe* for porting these worlds! This game would not be the same without you! - - - [Lobby (new edition)](https://mcpedl.com/mc-2017-new-mini-games-lobby-download-map/) - - [Lobby (old edition)](https://mcpedl.com/minecraft-2016-classic-mini-games-lobby-map/) - - [Arena](https://www.planetminecraft.com/project/minecraft-classic-tumble-mode-arena-download-java/) -2. Take note of the names of the world folders (you may rename them), we will need this in a moment. -3. Start and join your server. -4. Import both worlds into Multiverse. You can do this by running the command ```/mv import normal``` for both worlds. -5. Now you can link each world! Do this with ```/tumble:link lobby``` and ```/tumble:link game``` respectively. -6. Teleport to your new lobby world by using ```/mvtp ```. -7. Set the correct spawn location in this world using ```/setworldspawn```. For me, the correct coordinates were ```/setworldspawn -341.5 58 -340.5```, but your results may vary. -8. Set the location that the winner will be teleported using ```/tumble:winloc```. Again, the correct coordinates were ```/tumble:winloc -362.5 76 -340.5``` in my case. -9. Now, teleport to the game world. Use ```/mvtp ```. -10. Set the correct spawn point of this world. This is also where the game will generate its blocks. My preferred position is ```/setworldspawn 0 60 0```, but you may place the spawn whereever you like. - -You're done! - -## Continuation - -With this, the setup for this plugin is complete, but there still may be more for you to do. There are other plugins out there to fine-tune your experience even more. Plugins like [WorldGuard](https://dev.bukkit.org/projects/worldguard) and [CyberWorldReset](https://www.spigotmc.org/resources/cyberworldreset-standard-%E2%9C%A8-regenerate-worlds-scheduled-resets-lag-optimized%E3%80%8C1-8-1-19%E3%80%8D.96834/) can protect players from breaking blocks in the lobby and reset any redstone they activated, while others like [ViaVersion](https://www.spigotmc.org/resources/viaversion.19254/) can allow you to play Tumble from your favorite Minecraft version (yes, you, 1.8.9 players). - -Whatever you choose, the experience is up to you. - -Happy playing! diff --git a/src/main/java/com/MylesAndMore/Tumble/Main.java b/src/main/java/com/MylesAndMore/Tumble/Main.java index c264498..38c6d63 100644 --- a/src/main/java/com/MylesAndMore/Tumble/Main.java +++ b/src/main/java/com/MylesAndMore/Tumble/Main.java @@ -1,46 +1,41 @@ package com.MylesAndMore.Tumble; import com.MylesAndMore.Tumble.commands.*; -import com.MylesAndMore.Tumble.plugin.Constants; -import com.MylesAndMore.Tumble.plugin.EventListener; - -import com.jeff_media.updatechecker.UpdateCheckSource; -import com.jeff_media.updatechecker.UpdateChecker; +import com.MylesAndMore.Tumble.config.*; +import com.MylesAndMore.Tumble.game.Arena; import org.bstats.bukkit.Metrics; -import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; -public class Main extends JavaPlugin{ +import java.util.Objects; + +public class Main extends JavaPlugin { + public static Main plugin; + @Override public void onEnable() { - // Register setup items - getServer().getPluginManager().registerEvents(new EventListener(), this); - this.getCommand("reload").setExecutor(new Reload()); - this.getCommand("link").setExecutor(new SetWorldConfig()); - this.getCommand("start").setExecutor(new StartGame()); - this.getCommand("winlocation").setExecutor(new SetWinnerLoc()); - this.getCommand("autostart").setExecutor(new SetAutoStart()); - new Metrics(this, 16940); - this.saveDefaultConfig(); // Saves the default config file (packaged in the JAR) if we haven't already + plugin = this; - // Check if worlds are null in config and throw warnings if so - if (Constants.getGameWorld() == null) { - Bukkit.getServer().getLogger().warning("[Tumble] It appears you have not configured a game world for Tumble."); - Bukkit.getServer().getLogger().info("[Tumble] If this is your first time running the plugin, you may disregard this message."); - } - if (Constants.getLobbyWorld() == null) { - Bukkit.getServer().getLogger().warning("[Tumble] It appears you have not configured a lobby world for Tumble."); - Bukkit.getServer().getLogger().info("[Tumble] If this is your first time running the plugin, you may disregard this message."); - } + OldConfigManager.migrateConfig(); + LanguageManager.readConfig(); + SettingsManager.readConfig(); + ArenaManager.readConfig(); + LayerManager.readConfig(); - new UpdateChecker(this, UpdateCheckSource.SPIGET, "106721") - .setDownloadLink("https://github.com/MylesAndMore/Tumble/releases") - .setNotifyByPermissionOnJoin("tumble.update") // only this permission node is notified NOT all OPs so people can unsubscribe if they wish - .checkEveryXHours(336) // (every 2 weeks) - .checkNow(); + Objects.requireNonNull(this.getCommand("tumble")).setExecutor(new Tumble()); + new Metrics(this, 16940); + + this.getLogger().info("Tumble successfully enabled!"); + } - Bukkit.getServer().getLogger().info("[Tumble] Tumble successfully enabled!"); + @Override + public void onDisable() { + // Stop any running games + for (Arena a : ArenaManager.arenas.values()) { + if (a.game != null) { + a.game.stopGame(); + } + } } } \ No newline at end of file diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/Create.java b/src/main/java/com/MylesAndMore/Tumble/commands/Create.java new file mode 100644 index 0000000..d60ca64 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/Create.java @@ -0,0 +1,45 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Arena; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class Create implements SubCommand, CommandExecutor, TabCompleter { + @Override + public String getCommandName() { + return "create"; + } + + @Override + public String getPermission() { + return "tumble.create"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 0 || args[0] == null || args[0].isEmpty()) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + + String arenaName = args[0]; + ArenaManager.arenas.put(arenaName, new Arena(arenaName)); + ArenaManager.writeConfig(); + sender.sendMessage(LanguageManager.fromKey("create-success")); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/ForceStart.java b/src/main/java/com/MylesAndMore/Tumble/commands/ForceStart.java new file mode 100644 index 0000000..29583a4 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/ForceStart.java @@ -0,0 +1,71 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Game; +import com.MylesAndMore.Tumble.plugin.GameState; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class ForceStart implements SubCommand, CommandExecutor, TabCompleter { + + @Override + public String getCommandName() { + return "forcestart"; + } + + @Override + public String getPermission() { + return "tumble.forcestart"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + Game game; + + if (args.length < 1 || args[0] == null) { + // No arena passed in, try to infer from game player is in + game = ArenaManager.findGamePlayerIsIn((Player)sender); + if (game == null) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + } else { + String arenaName = args[0]; + if (!ArenaManager.arenas.containsKey(arenaName)) { + sender.sendMessage(LanguageManager.fromKey("invalid-arena").replace("%arena%",arenaName)); + return false; + } + game = ArenaManager.arenas.get(arenaName).game; + } + + if (game == null) { + sender.sendMessage(LanguageManager.fromKey("no-game-in-arena")); + return false; + } + + if (game.gameState != GameState.WAITING) { + return false; + } + + game.gameStart(); + sender.sendMessage(LanguageManager.fromKey("forcestart-success")); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + return ArenaManager.arenas.keySet().stream().toList(); + } + return new ArrayList<>(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/ForceStop.java b/src/main/java/com/MylesAndMore/Tumble/commands/ForceStop.java new file mode 100644 index 0000000..f308979 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/ForceStop.java @@ -0,0 +1,66 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Game; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class ForceStop implements SubCommand, CommandExecutor, TabCompleter { + + @Override + public String getCommandName() { + return "forcestop"; + } + + @Override + public String getPermission() { + return "tumble.forcestop"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + + Game game; + if (args.length < 1 || args[0] == null) { + // No arena passed in, try to infer from game player is in + game = ArenaManager.findGamePlayerIsIn((Player)sender); + if (game == null) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + } else { + String arenaName = args[0]; + if (!ArenaManager.arenas.containsKey(arenaName)) { + sender.sendMessage(LanguageManager.fromKey("invalid-arena").replace("%arena%",arenaName)); + return false; + } + game = ArenaManager.arenas.get(arenaName).game; + } + + if (game == null) { + sender.sendMessage(LanguageManager.fromKey("no-game-in-arena")); + return false; + } + + game.stopGame(); + sender.sendMessage(LanguageManager.fromKey("forcestop-success")); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + return ArenaManager.arenas.keySet().stream().toList(); + } + return new ArrayList<>(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/Join.java b/src/main/java/com/MylesAndMore/Tumble/commands/Join.java new file mode 100644 index 0000000..1771163 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/Join.java @@ -0,0 +1,134 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Arena; +import com.MylesAndMore.Tumble.game.Game; +import com.MylesAndMore.Tumble.plugin.GameState; +import com.MylesAndMore.Tumble.plugin.GameType; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Join implements SubCommand, CommandExecutor, TabCompleter { + + @Override + public String getCommandName() { + return "join"; + } + + @Override + public String getPermission() { + return "tumble.join"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + + if (!(sender instanceof Player p)) { + sender.sendMessage(LanguageManager.fromKey("not-for-console")); + return false; + } + + if (ArenaManager.findGamePlayerIsIn((Player)sender) != null) { + sender.sendMessage(LanguageManager.fromKey("already-in-game")); + return false; + } + + if (args.length < 1 || args[0] == null) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + String arenaName = args[0]; + if (!ArenaManager.arenas.containsKey(arenaName)) { + sender.sendMessage(LanguageManager.fromKey("invalid-arena").replace("%arena%", arenaName)); + return false; + } + Arena arena = ArenaManager.arenas.get(arenaName); + + // Check to make sure this arena has a game spawn + if (arena.gameSpawn == null) { + if (p.isOp()) { + sender.sendMessage(LanguageManager.fromKey("arena-not-ready-op")); + } else { + sender.sendMessage(LanguageManager.fromKey("arena-not-ready")); + } + return false; + } + + Game game; + if (args.length < 2 || args[1] == null) { + // No type specified: try to infer game type from game taking place in the arena + if (arena.game == null) { + // Can't infer if no game is taking place + sender.sendMessage(LanguageManager.fromKey("specify-game-type")); + return false; + } + + game = arena.game; + } else { + // Game type specified + GameType type; + switch (args[1]) { + case "shovels", "shovel" -> type = GameType.SHOVELS; + case "snowballs", "snowball" -> type = GameType.SNOWBALLS; + case "mix", "mixed" -> type = GameType.MIXED; + default -> { + sender.sendMessage(LanguageManager.fromKey("invalid-type")); + return false; + } + } + + if (arena.game == null) { + // No game is taking place in this arena, start one + game = arena.game = new Game(arena, type); + } else { + // A game is taking place in this arena, check that it is the right type + if (arena.game.type == type) { + game = arena.game; + } else { + sender.sendMessage(LanguageManager.fromKey("another-type-in-arena") + .replace("%type%", arena.game.type.toString()) + .replace("%arena%", arenaName)); + return false; + } + } + } + + // Make sure the game isn't in progress before adding the player + if (game.gameState != GameState.WAITING) { + sender.sendMessage(LanguageManager.fromKey("game-in-progress")); + return false; + } + + if (!game.addPlayer((Player)sender)) { + p.sendMessage(LanguageManager.fromKey("game-full")); + return false; + } + sender.sendMessage(LanguageManager.fromKey("join-success") + .replace("%type%", game.type.toString()) + .replace("%arena%", arena.name)); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + return ArenaManager.arenas.keySet().stream().toList(); + } + if (args.length == 2) { + return Arrays.stream(GameType.values()).map(Objects::toString).collect(Collectors.toList()); + } + return new ArrayList<>(); + } +} \ No newline at end of file diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/Leave.java b/src/main/java/com/MylesAndMore/Tumble/commands/Leave.java new file mode 100644 index 0000000..693f122 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/Leave.java @@ -0,0 +1,54 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Game; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class Leave implements SubCommand, CommandExecutor, TabCompleter { + + @Override + public String getCommandName() { + return "leave"; + } + + @Override + public String getPermission() { + return "tumble.leave"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + + if (!(sender instanceof Player)) { + sender.sendMessage(LanguageManager.fromKey("not-for-console")); + return false; + } + + Game game = ArenaManager.findGamePlayerIsIn((Player)sender); + if (game == null) { + sender.sendMessage(LanguageManager.fromKey("no-game-in-arena")); + return false; + } + + game.removePlayer((Player) sender); + sender.sendMessage(LanguageManager.fromKey("leave-success") + .replace("%arena%", game.arena.name) + .replace("%type%", game.type.toString())); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + return new ArrayList<>(); + } +} \ No newline at end of file diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/Reload.java b/src/main/java/com/MylesAndMore/Tumble/commands/Reload.java index ffc6dd8..ca7230c 100644 --- a/src/main/java/com/MylesAndMore/Tumble/commands/Reload.java +++ b/src/main/java/com/MylesAndMore/Tumble/commands/Reload.java @@ -1,22 +1,40 @@ package com.MylesAndMore.Tumble.commands; -import com.MylesAndMore.Tumble.plugin.Constants; -import org.bukkit.ChatColor; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.plugin.SubCommand; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; import org.jetbrains.annotations.NotNull; -public class Reload implements CommandExecutor { +import java.util.ArrayList; +import java.util.List; + +import static com.MylesAndMore.Tumble.Main.plugin; + +public class Reload implements SubCommand, CommandExecutor, TabCompleter { + + @Override + public String getCommandName() { + return "reload"; + } + @Override - public boolean onCommand(CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - if (sender.hasPermission("tumble.reload")) { - Constants.getPlugin().reloadConfig(); - sender.sendMessage(ChatColor.GREEN + "Tumble configuration reloaded successfully."); - } - else { - sender.sendMessage(ChatColor.RED + Constants.getPermissionMessage()); - } + public String getPermission() { + return "tumble.reload"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + plugin.onDisable(); + plugin.onEnable(); + sender.sendMessage(LanguageManager.fromKey("reload-success")); return true; } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + return new ArrayList<>(); + } } diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/Remove.java b/src/main/java/com/MylesAndMore/Tumble/commands/Remove.java new file mode 100644 index 0000000..9abc784 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/Remove.java @@ -0,0 +1,54 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class Remove implements SubCommand, CommandExecutor, TabCompleter { + + @Override + public String getCommandName() { + return "remove"; + } + + @Override + public String getPermission() { + return "tumble.remove"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 0 || args[0] == null || args[0].isEmpty()) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + String arenaName = args[0]; + + if (!ArenaManager.arenas.containsKey(arenaName)) { + sender.sendMessage(LanguageManager.fromKey("invalid-arena").replace("%arena%",arenaName)); + return false; + } + + ArenaManager.arenas.remove(arenaName); + ArenaManager.writeConfig(); + sender.sendMessage(LanguageManager.fromKey("remove-success")); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + return ArenaManager.arenas.keySet().stream().toList(); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/SetAutoStart.java b/src/main/java/com/MylesAndMore/Tumble/commands/SetAutoStart.java deleted file mode 100644 index b3da74e..0000000 --- a/src/main/java/com/MylesAndMore/Tumble/commands/SetAutoStart.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.MylesAndMore.Tumble.commands; - -import com.MylesAndMore.Tumble.plugin.Constants; -import org.bukkit.ChatColor; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -public class SetAutoStart implements CommandExecutor{ - @Override - public boolean onCommand(CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - if (sender.hasPermission("tumble.autostart")) { - if (Constants.getGameWorld() != null) { - if (Constants.getLobbyWorld() != null) { - if (args.length == 2) { - // Check the player # argument and parse it into an int - int args0; - try { - args0 = Integer.parseInt(args[0]); - } catch (NumberFormatException nfe){ - sender.sendMessage(ChatColor.RED + "Player amount must be a valid number."); - return true; - } catch (Exception e){ - sender.sendMessage(ChatColor.RED + "Invalid player amount."); - return true; - } - // PlayerAmount & enable/disable were entered - if ((args0 >= 2) && (args0 <= 8)) { - if (Objects.equals(args[1], "enable")) { - // Write values to the config - Constants.getPlugin().getConfig().set("autoStart.players", args0); - Constants.getPlugin().getConfig().set("autoStart.enabled", true); - Constants.getPlugin().saveConfig(); - sender.sendMessage(ChatColor.GREEN + "Configuration saved!"); - sender.sendMessage(ChatColor.GREEN + "Run " + ChatColor.GRAY + "/tumble:reload " + ChatColor.GREEN + "the changes to take effect."); - } - else if (Objects.equals(args[1], "disable")) { - Constants.getPlugin().getConfig().set("autoStart.players", args0); - Constants.getPlugin().getConfig().set("autoStart.enabled", false); - Constants.getPlugin().saveConfig(); - sender.sendMessage(ChatColor.GREEN + "Configuration saved!"); - sender.sendMessage(ChatColor.GREEN + "Run " + ChatColor.GRAY + "/tumble:reload " + ChatColor.GREEN + "the changes to take effect."); - } - else { - return false; - } - } - else { - sender.sendMessage(ChatColor.RED + "Please enter a player amount between two and eight!"); - } - } - else if (args.length == 1) { - // Only PlayerAmount was entered - int args0; - try { - args0 = Integer.parseInt(args[0]); - } catch (NumberFormatException nfe){ - sender.sendMessage(ChatColor.RED + "Player amount must be a valid number."); - return true; - } catch (Exception e){ - sender.sendMessage(ChatColor.RED + "Invalid player amount."); - return true; - } - if ((args0 >= 2) && (args0 <= 8)) { - Constants.getPlugin().getConfig().set("autoStart.players", args0); - Constants.getPlugin().saveConfig(); - sender.sendMessage(ChatColor.GREEN + "Configuration saved!"); - sender.sendMessage(ChatColor.GREEN + "Run " + ChatColor.GRAY + "/tumble:reload " + ChatColor.GREEN + "the changes to take effect."); - } - else { - sender.sendMessage(ChatColor.RED + "Please enter a player amount between two and eight!"); - } - } - else { - return false; - } - } - else { - sender.sendMessage(ChatColor.RED + "Please link a lobby world first!"); - } - } - else { - sender.sendMessage(ChatColor.RED + "Please link a game world first!"); - } - } - else { - sender.sendMessage(ChatColor.RED + Constants.getPermissionMessage()); - } - return true; - } -} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/SetGameSpawn.java b/src/main/java/com/MylesAndMore/Tumble/commands/SetGameSpawn.java new file mode 100644 index 0000000..817a1a4 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/SetGameSpawn.java @@ -0,0 +1,61 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Arena; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class SetGameSpawn implements SubCommand, CommandExecutor, TabCompleter { + @Override + public String getCommandName() { + return "setgamespawn"; + } + + @Override + public String getPermission() { + return "tumble.setgamespawn"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage(LanguageManager.fromKey("not-for-console")); + return false; + } + + if (args.length == 0 || args[0] == null || args[0].isEmpty()) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + String arenaName = args[0]; + + if (!ArenaManager.arenas.containsKey(arenaName)) { + sender.sendMessage(LanguageManager.fromKey("invalid-arena").replace("%arena%",arenaName)); + return false; + } + Arena arena = ArenaManager.arenas.get(arenaName); + + arena.gameSpawn = ((Player)sender).getLocation(); + ArenaManager.writeConfig(); + sender.sendMessage(LanguageManager.fromKey("set-success")); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + return ArenaManager.arenas.keySet().stream().toList(); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/SetKillYLevel.java b/src/main/java/com/MylesAndMore/Tumble/commands/SetKillYLevel.java new file mode 100644 index 0000000..27cc410 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/SetKillYLevel.java @@ -0,0 +1,62 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Arena; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class SetKillYLevel implements SubCommand, CommandExecutor, TabCompleter { + + @Override + public String getCommandName() { + return "setkillylevel"; + } + + @Override + public String getPermission() { + return "tumble.setkillylevel"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage(LanguageManager.fromKey("not-for-console")); + return false; + } + + if (args.length == 0 || args[0] == null || args[0].isEmpty()) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + String arenaName = args[0]; + + if (!ArenaManager.arenas.containsKey(arenaName)) { + sender.sendMessage(LanguageManager.fromKey("invalid-arena").replace("%arena%",arenaName)); + return false; + } + Arena arena = ArenaManager.arenas.get(arenaName); + + arena.killAtY = ((int) ((Player) sender).getLocation().getY()); + ArenaManager.writeConfig(); + sender.sendMessage(LanguageManager.fromKey("set-success")); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + return ArenaManager.arenas.keySet().stream().toList(); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/SetLobby.java b/src/main/java/com/MylesAndMore/Tumble/commands/SetLobby.java new file mode 100644 index 0000000..5708643 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/SetLobby.java @@ -0,0 +1,61 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Arena; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class SetLobby implements SubCommand, CommandExecutor, TabCompleter { + @Override + public String getCommandName() { + return "setlobby"; + } + + @Override + public String getPermission() { + return "tumble.setlobby"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage(LanguageManager.fromKey("not-for-console")); + return false; + } + + if (args.length == 0 || args[0] == null || args[0].isEmpty()) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + String arenaName = args[0]; + + if (!ArenaManager.arenas.containsKey(arenaName)) { + sender.sendMessage(LanguageManager.fromKey("invalid-arena").replace("%arena%",arenaName)); + return false; + } + Arena arena = ArenaManager.arenas.get(arenaName); + + arena.lobby = ((Player)sender).getLocation(); + ArenaManager.writeConfig(); + sender.sendMessage(LanguageManager.fromKey("set-success")); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + return ArenaManager.arenas.keySet().stream().toList(); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/SetWaitArea.java b/src/main/java/com/MylesAndMore/Tumble/commands/SetWaitArea.java new file mode 100644 index 0000000..7b90099 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/SetWaitArea.java @@ -0,0 +1,61 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Arena; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class SetWaitArea implements SubCommand, CommandExecutor, TabCompleter { + @Override + public String getCommandName() { + return "setwaitarea"; + } + + @Override + public String getPermission() { + return "tumble.setwaitarea"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage(LanguageManager.fromKey("not-for-console")); + return false; + } + + if (args.length == 0 || args[0] == null || args[0].isEmpty()) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + String arenaName = args[0]; + + if (!ArenaManager.arenas.containsKey(arenaName)) { + sender.sendMessage(LanguageManager.fromKey("invalid-arena").replace("%arena%",arenaName)); + return false; + } + Arena arena = ArenaManager.arenas.get(arenaName); + + arena.waitArea = ((Player)sender).getLocation(); + ArenaManager.writeConfig(); + sender.sendMessage(LanguageManager.fromKey("set-success")); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + return ArenaManager.arenas.keySet().stream().toList(); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/SetWinnerLobby.java b/src/main/java/com/MylesAndMore/Tumble/commands/SetWinnerLobby.java new file mode 100644 index 0000000..e0d2bea --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/SetWinnerLobby.java @@ -0,0 +1,61 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.ArenaManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.game.Arena; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class SetWinnerLobby implements SubCommand, CommandExecutor, TabCompleter { + @Override + public String getCommandName() { + return "setwinnerlobby"; + } + + @Override + public String getPermission() { + return "tumble.setwinnerlobby"; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (!(sender instanceof Player)) { + sender.sendMessage(LanguageManager.fromKey("not-for-console")); + return false; + } + + if (args.length == 0 || args[0] == null || args[0].isEmpty()) { + sender.sendMessage(LanguageManager.fromKey("missing-arena-parameter")); + return false; + } + String arenaName = args[0]; + + if (!ArenaManager.arenas.containsKey(arenaName)) { + sender.sendMessage(LanguageManager.fromKey("invalid-arena").replace("%arena%",arenaName)); + return false; + } + Arena arena = ArenaManager.arenas.get(arenaName); + + arena.winnerLobby = ((Player)sender).getLocation(); + ArenaManager.writeConfig(); + sender.sendMessage(LanguageManager.fromKey("set-success")); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + return ArenaManager.arenas.keySet().stream().toList(); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/SetWinnerLoc.java b/src/main/java/com/MylesAndMore/Tumble/commands/SetWinnerLoc.java deleted file mode 100644 index 38e6444..0000000 --- a/src/main/java/com/MylesAndMore/Tumble/commands/SetWinnerLoc.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.MylesAndMore.Tumble.commands; - -import com.MylesAndMore.Tumble.plugin.Constants; -import org.bukkit.ChatColor; -import org.bukkit.Location; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.ConsoleCommandSender; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -public class SetWinnerLoc implements CommandExecutor { - @Override - public boolean onCommand(CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - if (sender.hasPermission("tumble.winlocation")) { - if (Constants.getLobbyWorld() != null) { - if (sender instanceof Player) { - // Check the sender entered the correct number of args - if (args.length == 3) { - double args0 = 0; - double args1 = 0; - double args2 = 0; - try { - args0 = Double.parseDouble(args[0]); - args1 = Double.parseDouble(args[1]); - args2 = Double.parseDouble(args[2]); - } catch (NumberFormatException nfe){ - sender.sendMessage(ChatColor.RED + "Input arguments must be valid numbers."); - } catch (Exception e){ - sender.sendMessage(ChatColor.RED + "Invalid input arguments."); - } - // Check if any of the args were 0 (this will cause future problems, so we prevent it here) - if (!((args0 == 0) || (args1 == 0) || (args2 == 0))) { - Constants.getPlugin().getConfig().set("winnerTeleport.x", args0); - Constants.getPlugin().getConfig().set("winnerTeleport.y", args1); - Constants.getPlugin().getConfig().set("winnerTeleport.z", args2); - Constants.getPlugin().saveConfig(); - sender.sendMessage(ChatColor.GREEN + "Win location successfully set!"); - sender.sendMessage(ChatColor.GREEN + "Run " + ChatColor.GRAY + "/tumble:reload " + ChatColor.GREEN + "the changes to take effect."); - } - else { - sender.sendMessage(ChatColor.RED + "Your coordinates cannot be zero!"); - sender.sendMessage(ChatColor.RED + "Use something like 0.5 (the middle of the block) instead."); - } - } - // If the sender entered no args, use their current location - else if (args.length == 0) { - Location senderPos = ((Player) sender).getLocation(); - // if so, check if any of their locations are zero - if (!((senderPos.getX() == 0) || (senderPos.getY() == 0) || (senderPos.getZ() == 0))) { - // set the config values to their current pos - Constants.getPlugin().getConfig().set("winnerTeleport.x", senderPos.getX()); - Constants.getPlugin().getConfig().set("winnerTeleport.y", senderPos.getY()); - Constants.getPlugin().getConfig().set("winnerTeleport.z", senderPos.getZ()); - Constants.getPlugin().saveConfig(); - sender.sendMessage(ChatColor.GREEN + "Win location successfully set!"); - sender.sendMessage(ChatColor.GREEN + "Run " + ChatColor.GRAY + "/tumble:reload " + ChatColor.GREEN + "the changes to take effect."); - } - else { - sender.sendMessage(ChatColor.RED + "Your coordinates cannot be zero!"); - sender.sendMessage(ChatColor.RED + "Use something like 0.5 (the middle of the block) instead."); - } - } - else { - return false; - } - } - else if (sender instanceof ConsoleCommandSender) { - if (args.length == 3) { - double args0 = 0; - double args1 = 0; - double args2 = 0; - try { - args0 = Double.parseDouble(args[0]); - args1 = Double.parseDouble(args[1]); - args2 = Double.parseDouble(args[2]); - } catch (NumberFormatException nfe){ - sender.sendMessage(ChatColor.RED + "Input arguments must be valid numbers."); - } catch (Exception e){ - sender.sendMessage(ChatColor.RED + "Invalid input arguments."); - } - if (!((args0 == 0) || (args1 == 0) || (args2 == 0))) { - Constants.getPlugin().getConfig().set("winnerTeleport.x", args0); - Constants.getPlugin().getConfig().set("winnerTeleport.y", args1); - Constants.getPlugin().getConfig().set("winnerTeleport.z", args2); - Constants.getPlugin().saveConfig(); - sender.sendMessage(ChatColor.GREEN + "Win location successfully set!"); - sender.sendMessage(ChatColor.GREEN + "Run " + ChatColor.GRAY + "/tumble:reload " + ChatColor.GREEN + "the changes to take effect."); - } - else { - sender.sendMessage(ChatColor.RED + "Your coordinates cannot be zero!"); - sender.sendMessage(ChatColor.RED + "Use something like 0.5 (the middle of the block) instead."); - } - } - else { - return false; - } - } - } - else { - sender.sendMessage(ChatColor.RED + "Please link a lobby world first!"); - } - } - else { - sender.sendMessage(ChatColor.RED + Constants.getPermissionMessage()); - } - return true; - } -} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/SetWorldConfig.java b/src/main/java/com/MylesAndMore/Tumble/commands/SetWorldConfig.java deleted file mode 100644 index 90e0a96..0000000 --- a/src/main/java/com/MylesAndMore/Tumble/commands/SetWorldConfig.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.MylesAndMore.Tumble.commands; - -import com.MylesAndMore.Tumble.plugin.Constants; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.GameRule; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -public class SetWorldConfig implements CommandExecutor { - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - // Catch for null arguments - if (args.length == 2) { - if (sender.hasPermission("tumble.link")){ - // Initialize vars for their respective command arguments - String world = args[0]; - String worldType = args[1]; - if (Objects.equals(worldType, "lobby")) { - // Check if the world is actually a world on the server - if (Bukkit.getWorld(world) != null) { - // Check if the world has already been configured - if (!Objects.equals(Constants.getGameWorld(), world)) { - // Set the specified value of the world in the config under lobbyWorld - Constants.getPlugin().getConfig().set("lobbyWorld", world); - Constants.getPlugin().saveConfig(); - sender.sendMessage(ChatColor.GREEN + "Lobby world successfully linked: " + ChatColor.GRAY + world); - sender.sendMessage(ChatColor.GREEN + "Please restart your server for the changes to take effect; " + ChatColor.RED + "reloading the plugin is insufficient!"); - } - else { - sender.sendMessage(ChatColor.RED + "That world has already been linked, please choose/create another world!"); - } - } - else { - sender.sendMessage(ChatColor.RED + "Failed to find a world named " + ChatColor.GRAY + world); - } - } - else if (Objects.equals(args[1], "game")) { - if (Bukkit.getWorld(world) != null) { - if (!Objects.equals(Constants.getLobbyWorld(), world)) { - Constants.getPlugin().getConfig().set("gameWorld", world); - Constants.getPlugin().saveConfig(); - // Set the gamerule of doImmediateRespawn in the gameWorld for later - Objects.requireNonNull(Bukkit.getWorld(world)).setGameRule(GameRule.DO_IMMEDIATE_RESPAWN, true); - Objects.requireNonNull(Bukkit.getWorld(world)).setGameRule(GameRule.KEEP_INVENTORY, true); - sender.sendMessage(ChatColor.GREEN + "Game world successfully linked: " + ChatColor.GRAY + world); - sender.sendMessage(ChatColor.GREEN + "Please restart your server for the changes to take effect; " + ChatColor.RED + "reloading the plugin is insufficient!"); - } - else { - sender.sendMessage(ChatColor.RED + "That world has already been linked, please choose/create another world!"); - } - } - else { - sender.sendMessage(ChatColor.RED + "Failed to find a world named " + ChatColor.GRAY + world); - } - } - else { - sender.sendMessage(ChatColor.RED + "Allowed world types are " + ChatColor.GRAY + "lobby " + ChatColor.RED + "and " + ChatColor.GRAY + "game" + ChatColor.RED + "."); - } - } - else { - sender.sendMessage(ChatColor.RED + Constants.getPermissionMessage()); - } - } - else { - return false; - } - return true; - } -} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/StartGame.java b/src/main/java/com/MylesAndMore/Tumble/commands/StartGame.java deleted file mode 100644 index 706b33a..0000000 --- a/src/main/java/com/MylesAndMore/Tumble/commands/StartGame.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.MylesAndMore.Tumble.commands; - -import com.MylesAndMore.Tumble.game.Game; -import com.MylesAndMore.Tumble.plugin.Constants; -import org.bukkit.ChatColor; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.Objects; - -public class StartGame implements CommandExecutor { - @Override - public boolean onCommand(CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - if (sender.hasPermission("tumble.start")) { - if (Constants.getLobbyWorld() != null) { - if (Constants.getPlayersInLobby().size() > 1) { - if (Constants.getGameWorld() != null) { - if (!Objects.equals(Game.getGame().getGameState(), "waiting")) { - sender.sendMessage(ChatColor.BLUE + "Generating layers, please wait."); - // Use multiverse to load game world--if the load was successful, start game - if (Constants.getMVWorldManager().loadWorld(Constants.getGameWorld())) { - // If there is no starting argument, - if (args.length == 0) { - // pull which gamemode to initiate from the config file - if (!Game.getGame().startGame(Constants.getGameType())) { - // Sender feedback for if the game failed to start - if (Objects.equals(Game.getGame().getGameState(), "starting")) { - sender.sendMessage(ChatColor.RED + "A game is already starting!"); - } - else if (Objects.equals(Game.getGame().getGameState(), "running")) { - sender.sendMessage(ChatColor.RED + "A game is already running!"); - } - else { - sender.sendMessage(ChatColor.RED + "Failed to recognize game of type " + ChatColor.GRAY + Constants.getPlugin().getConfig().getString("gameMode")); - } - } - } - // If there was an argument for gameType, pass that instead - else { - if (!Game.getGame().startGame(args[0])) { - // Sender feedback for if the game failed to start - if (Objects.equals(Game.getGame().getGameState(), "starting")) { - sender.sendMessage(ChatColor.RED + "A game is already starting!"); - } - else if (Objects.equals(Game.getGame().getGameState(), "running")) { - sender.sendMessage(ChatColor.RED + "A game is already running!"); - } - else { - sender.sendMessage(ChatColor.RED + "Failed to recognize game of type " + ChatColor.GRAY + args[0]); - } - } - } - } - // If load was unsuccessful, give feedback - // Note: this should not occur unless the config file was edited externally, - // because the plugin prevents adding "worlds" that are not actually present to the config. - else { - sender.sendMessage(ChatColor.RED + "Failed to find a world named " + ChatColor.GRAY + Constants.getGameWorld()); - sender.sendMessage(ChatColor.RED + "Is the configuration file correct?"); - } - } - else { - sender.sendMessage(ChatColor.RED + "A game is already queued to begin!"); - } - } - else { - sender.sendMessage(ChatColor.RED + "Please link a game world first!"); - } - } - else { - sender.sendMessage(ChatColor.RED + "You can't start a game with yourself!"); - } - } - else { - sender.sendMessage(ChatColor.RED + "Please link a lobby world first!"); - } - } - else { - sender.sendMessage(ChatColor.RED + Constants.getPermissionMessage()); - } - return true; - } -} diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/Tumble.java b/src/main/java/com/MylesAndMore/Tumble/commands/Tumble.java new file mode 100644 index 0000000..73acf6b --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/commands/Tumble.java @@ -0,0 +1,106 @@ +package com.MylesAndMore.Tumble.commands; + +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.plugin.SubCommand; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class Tumble implements CommandExecutor, TabCompleter { + + private static final Map subCommands = Map.ofEntries( + cmdNameAsKey(new Create()), + cmdNameAsKey(new ForceStart()), + cmdNameAsKey(new ForceStop()), + cmdNameAsKey(new Join()), + cmdNameAsKey(new Leave()), + cmdNameAsKey(new Reload()), + cmdNameAsKey(new Remove()), + cmdNameAsKey(new SetGameSpawn()), + cmdNameAsKey(new SetKillYLevel()), + cmdNameAsKey(new SetLobby()), + cmdNameAsKey(new SetWaitArea()), + cmdNameAsKey(new SetWinnerLobby()) + ); + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 0 || args[0] == null || args[0].isEmpty()) { + sender.sendMessage(LanguageManager.fromKey("missing-subcommand")); + return true; + } + String subCmdName = args[0]; + + if (!subCommands.containsKey(subCmdName)) { + sender.sendMessage(LanguageManager.fromKey("unknown-command").replace("%command%", subCmdName)); + return true; + } + + var subCmd = subCommands.get(subCmdName); + + if (!sender.hasPermission(subCmd.getPermission())) { + sender.sendMessage(LanguageManager.fromKey("no-permission").replace("%permission%", subCmd.getPermission())); + return true; + } + + // Pass command action through to subCommand + subCmd.onCommand(sender, command, subCmdName, removeFirst(args)); + return true; + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + if (args.length == 1) { + // Show only subCommands the user has permission for + ArrayList PermittedSubCmds = new ArrayList<>(); + for (SubCommand subCmd: subCommands.values()) { + if (sender.hasPermission(subCmd.getPermission())) { + PermittedSubCmds.add(subCmd.getCommandName()); + } + } + return PermittedSubCmds; + } + + if (args.length > 1) { + if (!subCommands.containsKey(args[0])) { + return Collections.emptyList(); + } + + // Pass tab complete through to subCommand + if (subCommands.get(args[0]) instanceof TabCompleter tcmp) { + return tcmp.onTabComplete(sender, command, args[0], removeFirst(args)); + } else { + return null; + } + } + + return Collections.emptyList(); + } + + /** + * Create a copy of an array with the first element removed + * @param arr the source array + * @return the source without the first element + */ + private String[] removeFirst(String[] arr) { + ArrayList tmp = new ArrayList<>(List.of(arr)); + tmp.remove(0); + return tmp.toArray(new String[0]); + } + + /** + * Creates a map entry with the name of the subCommand as the key and the subCommand itself as the value + * @param cmd The subCommand to use + * @return A map entry from the subCommand + */ + private static Map.Entry cmdNameAsKey(SubCommand cmd) { + return Map.entry(cmd.getCommandName(),cmd); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/config/ArenaManager.java b/src/main/java/com/MylesAndMore/Tumble/config/ArenaManager.java new file mode 100644 index 0000000..84f5595 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/config/ArenaManager.java @@ -0,0 +1,186 @@ +package com.MylesAndMore.Tumble.config; + +import com.MylesAndMore.Tumble.game.Arena; +import com.MylesAndMore.Tumble.game.Game; +import com.MylesAndMore.Tumble.plugin.CustomConfig; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Objects; + +import static com.MylesAndMore.Tumble.Main.plugin; + +/** + * Manages arenas.yml and stores list of arenas + */ +public class ArenaManager { + public static HashMap arenas; + + private static CustomConfig arenasYml; + private static FileConfiguration config; + + /** + * Read arenas from arenas.yml and populate this.arenas + */ + public static void readConfig() { + arenasYml = new CustomConfig("arenas.yml"); + arenasYml.saveDefaultConfig(); + config = arenasYml.getConfig(); + arenas = new HashMap<>(); + + ConfigurationSection arenasSection = config.getConfigurationSection("arenas"); + if (arenasSection == null) { + plugin.getLogger().warning("arenas.yml is missing key 'arenas'"); + return; + } + for (String arenaName: arenasSection.getKeys(false)) { + Arena arena = new Arena(arenaName); + + if (config.contains("arenas." + arenaName + ".kill-at-y", true)) { + arena.killAtY = config.getInt("arenas." + arenaName + ".kill-at-y"); + } + if (config.contains("arenas." + arenaName + ".game-spawn")) { + arena.gameSpawn = readWorld("arenas." + arenaName + ".game-spawn"); + } + if (config.contains("arenas." + arenaName + ".lobby")) { + arena.lobby = readWorld("arenas." + arenaName + ".lobby"); + } + if (config.contains("arenas." + arenaName + ".winner-lobby")) { + arena.winnerLobby = readWorld("arenas." + arenaName + ".winner-lobby"); + } + if (config.contains("arenas." + arenaName + ".wait-area")) { + arena.waitArea = readWorld("arenas." + arenaName + ".wait-area"); + } + + arenas.put(arena.name, arena); + } + validate(); // Validate arenas + plugin.getLogger().info("arenas.yml: Loaded " + arenas.size() + (arenas.size() > 1 ? " arenas" : " arena")); + } + + /** + * Write arenas from this.arenas to arenas.yml + */ + public static void writeConfig() { + config.set("arenas", null); // Clear everything + + for (Arena arena: arenas.values()) { + if (arena.killAtY != null) { + config.set("arenas." + arena.name + ".kill-at-y", arena.killAtY); + } + if (arena.gameSpawn != null) { + writeWorld("arenas." + arena.name + ".game-spawn", arena.gameSpawn); + } + if (arena.lobby != null) { + writeWorld("arenas." + arena.name + ".lobby", arena.lobby); + } + if (arena.winnerLobby != null) { + writeWorld("arenas." + arena.name + ".winner-lobby", arena.winnerLobby); + } + if (arena.waitArea != null) { + writeWorld("arenas." + arena.name + ".wait-area", arena.waitArea); + } + } + + validate(); + arenasYml.saveConfig(); + } + + /** + * Check that all arenas are valid + */ + public static void validate() { + for (Arena arena: arenas.values()) { + if (arena.gameSpawn == null) { + plugin.getLogger().severe("arenas.yml: Arena '" + arena.name + "' is missing a game spawn, before it is usable you must set a spawn with '/tumble setgamespawn'."); + } + if (arena.lobby == null) { + plugin.getLogger().warning("arenas.yml: Arena '" + arena.name + "' is missing a lobby location. The spawn point of the default world will be used."); + } + } + } + + /** + * Searches all arenas for a game that player p is in + * @param p Player to search for + * @return the game the player is in, or null if not found + */ + public static Game findGamePlayerIsIn(Player p) { + for (Arena a : arenas.values()) { + if (a.game != null && a.game.gamePlayers.contains(p)) { + return a.game; + } + } + return null; + } + + /** + * Tries to convert a config section in the following format to a world + * section: + * x: + * y: + * z: + * world: + * @param path The section in the yaml with x, y, z, and world as its children + * @return The location specified by the section, or null if the location is not valid + */ + private static Location readWorld(String path) { + ConfigurationSection section = config.getConfigurationSection(path); + if (section == null) { + plugin.getLogger().warning("arenas.yml: Error loading location at '" + path + "' - " + "Section is null"); + return null; + } + + double x = section.getDouble("x"); + double y = section.getDouble("y"); + double z = section.getDouble("z"); + if (x == 0 || y == 0 || z == 0) { + plugin.getLogger().warning("arenas.yml: Error loading location at '" + path + "' - " + "Arena coordinates are missing or are zero. Coordinates cannot be zero."); + return null; + } + + String worldName = section.getString("world"); + if (worldName == null) { + plugin.getLogger().warning("arenas.yml: Error loading location at '" + path +"' - " + "World name is missing"); + return null; + } + + World world = Bukkit.getWorld(worldName); + if (world == null) { + plugin.getLogger().warning("arenas.yml: Error loading location at '" + path + "' - " + "Failed to load world '" + worldName + "'"); + return null; + } + + return new Location(world,x,y,z); + } + + /** + * Write a location into the config using the following format: + * section: + * x: + * y: + * z: + * world: + * @param path The path of the section to write + * @param location The location to write + */ + private static void writeWorld(String path, @NotNull Location location) { + ConfigurationSection section = config.getConfigurationSection(path); + + if (section == null) { + section = config.createSection(path); + } + + section.set("x", location.getX()); + section.set("y", location.getY()); + section.set("z", location.getZ()); + section.set("world", Objects.requireNonNull(location.getWorld()).getName()); + + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/config/LanguageManager.java b/src/main/java/com/MylesAndMore/Tumble/config/LanguageManager.java new file mode 100644 index 0000000..6625b26 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/config/LanguageManager.java @@ -0,0 +1,72 @@ +package com.MylesAndMore.Tumble.config; + +import com.MylesAndMore.Tumble.plugin.CustomConfig; +import org.bukkit.ChatColor; +import org.bukkit.configuration.Configuration; + +import java.util.Objects; + +import static com.MylesAndMore.Tumble.Main.plugin; + +/** + * Manages language.yml and allows retrieval of keys + */ +public class LanguageManager { + private static Configuration config; + private static Configuration defaultConfig; + + public static void readConfig() { + CustomConfig languageYml = new CustomConfig("language.yml"); + languageYml.saveDefaultConfig(); + config = languageYml.getConfig(); + defaultConfig = Objects.requireNonNull(config.getDefaults()); + + validate(); + } + + /** + * Check keys of language.yml against the defaults + */ + public static void validate() { + boolean invalid = false; + for (String key : defaultConfig.getKeys(true)) { + if (!config.contains(key,true)) { + plugin.getLogger().warning("language.yml is missing key '" + key + "'."); + invalid = true; + } + } + if (invalid) { + plugin.getLogger().severe("Errors were found in language.yml, default values will be used."); + } + } + + /** + * Gets a key from language.yml and prepends the prefix. + * If it is not present, a default value will be returned + * @param key The key representing the message + * @return The message from the key + */ + public static String fromKey(String key) { + return fromKeyNoPrefix("prefix") + fromKeyNoPrefix(key); + } + + /** + * Gets a key from language.yml. + * If it is not present, a default value will be returned + * @param key The key representing the message + * @return The message from the key + */ + public static String fromKeyNoPrefix(String key) { + String val = config.getString(key); + + if (val == null) { + val = defaultConfig.getString(key); + } + + if (val == null) { + val = "LANG_ERR"; + } + + return ChatColor.translateAlternateColorCodes('&',val); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/config/LayerManager.java b/src/main/java/com/MylesAndMore/Tumble/config/LayerManager.java new file mode 100644 index 0000000..e2fe310 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/config/LayerManager.java @@ -0,0 +1,139 @@ +package com.MylesAndMore.Tumble.config; + +import com.MylesAndMore.Tumble.plugin.CustomConfig; +import org.bukkit.Material; +import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.ConfigurationSection; + +import java.util.*; + +import static com.MylesAndMore.Tumble.Main.plugin; + +public class LayerManager { + public static List> layers; + private static int layerCount; + + private static final List unsafeMaterials = List.of( + Material.COBWEB, + Material.MAGMA_BLOCK, + Material.CAMPFIRE, + Material.VINE, + Material.AIR + ); + + /** + * Read layers from layers.yml and populate this.layers + */ + public static void readConfig() { + CustomConfig layersYml = new CustomConfig("layers.yml"); + Configuration config = layersYml.getConfig(); + Configuration defaultConfig = Objects.requireNonNull(config.getDefaults()); + + layers = new ArrayList<>(); + layerCount = 0; + layersYml.saveDefaultConfig(); + + readLayers(config.getConfigurationSection("layers")); + if (layers.isEmpty()) { + plugin.getLogger().warning("layers.yml: No layers were found, using defaults"); + readLayers(defaultConfig.getConfigurationSection("layers")); + } + + // Don't use layers.size() because it includes duplicates for weighted layers + plugin.getLogger().info("layers.yml: Loaded " + layerCount + (layerCount == 1 ? " layer" : " layers")); + } + + /** + * Read the layers from the layers.yml file + * @param section The 'layers' section of the config + */ + public static void readLayers(ConfigurationSection section) { + if (section == null) { + plugin.getLogger().warning("layers.yml is missing section 'layers', using defaults"); + return; + } + + for (String layerPath : section.getKeys(false)) { + ConfigurationSection layerSection = section.getConfigurationSection(layerPath); + if (layerSection == null) { + plugin.getLogger().warning("layers.yml: Layer '" + layerPath + "' is null"); + continue; + } + List layer = readLayer(layerSection); + if (layer == null) { + plugin.getLogger().warning("layers.yml: Failed to load layer '" + layerPath + "'"); + continue; + } + + int weight = layerSection.getInt("weight", 1); + layerCount++; + for (int i = 0; i < weight; i++) { + layers.add(layer); + } + } + } + + /** + * Read the list of materials for a layer + * @param section The path of the layer in the config + * @return The list of materials for the layer to be composed of + */ + public static List readLayer(ConfigurationSection section) { + List materialsList = section.getStringList("materials"); + if (materialsList.isEmpty()) { + plugin.getLogger().warning("layers.yml: Layer '" + section.getCurrentPath() + "' is missing section 'materials'"); + return null; + } + + List materials = new ArrayList<>(); + for (String s : materialsList) { + String[] sp = s.split(" "); + + if (sp.length < 1) { + plugin.getLogger().warning("layers.yml: Invalid format in layer '" + section.getCurrentPath() + "'"); + continue; + } + String matName = sp[0]; + Material mat = Material.getMaterial(matName); + if (mat == null) { + plugin.getLogger().warning("layers.yml: Invalid material '" + matName + "' in layer '" + section.getCurrentPath() + "'"); + continue; + } + + int matWeight; + if (sp.length < 2) { + matWeight = 1; + } else { + try { + matWeight = Integer.parseInt(sp[1]); + } catch (NumberFormatException e) { + plugin.getLogger().warning("layers.yml: Invalid weight '" + sp[1] + "' in layer '" + section.getCurrentPath() + "'"); + matWeight = 1; + } + } + + for (int i = 0; i < matWeight; i++) { + materials.add(mat); + } + } + return materials; + } + + /** + * Selects a random layer for use in the generator. + * @return A random layer + */ + public static List getRandomLayer() { + return layers.get(new Random().nextInt(layers.size())); + } + + /** + * Selects a random layer and removes materials that are unsafe for players to stand on. + * @return A random safe layer + */ + public static List getRandomLayerSafe() { + List ret = new ArrayList<>(getRandomLayer()); // Deep copy + ret.removeAll(unsafeMaterials); + return ret; + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/config/OldConfigManager.java b/src/main/java/com/MylesAndMore/Tumble/config/OldConfigManager.java new file mode 100644 index 0000000..d3dec2f --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/config/OldConfigManager.java @@ -0,0 +1,97 @@ +package com.MylesAndMore.Tumble.config; + +import com.MylesAndMore.Tumble.game.Arena; +import com.MylesAndMore.Tumble.plugin.CustomConfig; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.file.FileConfiguration; + +import java.io.File; + +import static com.MylesAndMore.Tumble.Main.plugin; + +public class OldConfigManager { + /** + * Populate the new configs with the data from the old (v1.x) config.yml and rename to config_old.yml if necessary + */ + public static void migrateConfig() { + boolean needsMigration = new File(plugin.getDataFolder(), "config.yml").exists() && !new File(plugin.getDataFolder(), "arenas.yml").exists(); + if (!needsMigration) { + return; + } + plugin.getLogger().info("Converting config.yml..."); + + CustomConfig configYml = new CustomConfig("config.yml"); + FileConfiguration oldConfig = configYml.getConfig(); + + // Collect data from old config + boolean autoStartEnabled = oldConfig.getBoolean("autoStart.enabled"); + boolean hideJoinLeaveMessages = oldConfig.getBoolean("hideJoinLeaveMessages"); + String permissionMessage = oldConfig.getString("permissionMessage"); + double winnerTeleportX = oldConfig.getDouble("winnerTeleport.x"); + double winnerTeleportY = oldConfig.getDouble("winnerTeleport.y"); + double winnerTeleportZ = oldConfig.getDouble("winnerTeleport.z"); + String lobbyWorldName = oldConfig.getString("lobbyWorld"); + String gameWorldName = oldConfig.getString("gameWorld"); + + World lobbyWorld = lobbyWorldName == null ? null : Bukkit.getWorld(lobbyWorldName); + World gameWorld = gameWorldName == null ? null : Bukkit.getWorld(gameWorldName); + + // Create arena with info from config + Arena a = new Arena("default"); + if (lobbyWorld != null) { + a.lobby = normalizeLocation(new Location(lobbyWorld, lobbyWorld.getSpawnLocation().getX(), lobbyWorld.getSpawnLocation().getY(), lobbyWorld.getSpawnLocation().getZ())); + if (winnerTeleportX != 0 || winnerTeleportY != 0 || winnerTeleportZ != 0) { + a.winnerLobby = normalizeLocation(new Location(lobbyWorld, winnerTeleportX, winnerTeleportY, winnerTeleportZ)); + } + } + if (gameWorld != null) { + a.gameSpawn = normalizeLocation(new Location(gameWorld, gameWorld.getSpawnLocation().getX(), gameWorld.getSpawnLocation().getY(), gameWorld.getSpawnLocation().getZ())); + // Game world is required so the arena will only be added in this case + ArenaManager.readConfig(); + ArenaManager.arenas.put(a.name, a); + ArenaManager.writeConfig(); + } + + // Move permission message to language.yml + // Skip migration if they left the message as the (old) default + if (permissionMessage != null && !permissionMessage.equals("You do not have permission to perform this command!")) { + CustomConfig languagesYml = new CustomConfig("language.yml"); + languagesYml.saveDefaultConfig(); + FileConfiguration languagesConfig = languagesYml.getConfig(); + languagesConfig.set("no-permission", permissionMessage); + languagesYml.saveConfig(); + } + + // Move hide-join-leave-messages and autostart to settings.yml + if (hideJoinLeaveMessages || autoStartEnabled) { + CustomConfig settingsYml = new CustomConfig("settings.yml"); + settingsYml.saveDefaultConfig(); + FileConfiguration settingsConfig = settingsYml.getConfig(); + settingsConfig.set("hide-join-leave-messages", true); + // wait-duration should stay as default unless autostart was disabled + if (!autoStartEnabled) { + settingsConfig.set("wait-duration", 0); + } + settingsYml.saveConfig(); + } + + if (!new File(plugin.getDataFolder(), "config.yml").renameTo(new File(plugin.getDataFolder(), "config_old.yml"))) { + plugin.getLogger().severe("Failed to rename config.yml to config_old.yml. Please manually rename this to avoid data loss."); + } + plugin.getLogger().info("Conversion complete! Please restart the server to apply changes."); + } + + /** + * Normalize a location to allow it to be saved to a config + * @param loc The location to normalize + * @return The normalized location + */ + private static Location normalizeLocation(Location loc) { + return new Location(loc.getWorld(), + loc.getX() == 0.0 ? 0.5 : loc.getX(), + loc.getY() == 0.0 ? 0.5 : loc.getY(), + loc.getZ() == 0.0 ? 0.5 : loc.getZ()); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/config/SettingsManager.java b/src/main/java/com/MylesAndMore/Tumble/config/SettingsManager.java new file mode 100644 index 0000000..1ffc043 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/config/SettingsManager.java @@ -0,0 +1,51 @@ +package com.MylesAndMore.Tumble.config; + +import com.MylesAndMore.Tumble.plugin.CustomConfig; +import org.bukkit.configuration.Configuration; + +import java.util.Objects; + +import static com.MylesAndMore.Tumble.Main.plugin; + +/** + * Manages settings.yml and stores its options + */ +public class SettingsManager { + public static boolean hideLeaveJoin; + public static boolean hideDeathMessages; + public static int waitDuration; + + private static Configuration config; + private static Configuration defaultConfig; + + /** + * Reads options in from settings.yml + */ + public static void readConfig() { + CustomConfig settingsYml = new CustomConfig("settings.yml"); + settingsYml.saveDefaultConfig(); + config = settingsYml.getConfig(); + defaultConfig = Objects.requireNonNull(config.getDefaults()); + hideLeaveJoin = config.getBoolean("hide-join-leave-messages", false); + hideDeathMessages = config.getBoolean("hide-death-messages", false); + waitDuration = config.getInt("wait-duration", 15); + + validate(); + } + + /** + * Check keys of settings.yml against the defaults + */ + public static void validate() { + boolean invalid = false; + for (String key : defaultConfig.getKeys(true)) { + if (!config.contains(key,true)) { + plugin.getLogger().warning("settings.yml is missing key '" + key + "'."); + invalid = true; + } + } + if (invalid) { + plugin.getLogger().severe("Errors were found in settings.yml, default values will be used."); + } + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/game/Arena.java b/src/main/java/com/MylesAndMore/Tumble/game/Arena.java new file mode 100644 index 0000000..157c59c --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/game/Arena.java @@ -0,0 +1,28 @@ +package com.MylesAndMore.Tumble.game; + +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; + +/** + * An arena is the world and spawn location where a game can take place. An arena can only host one game at a time. + */ +public class Arena { + + public final String name; + + public Integer killAtY = null; + public Location gameSpawn = null; + public Location lobby = null; + public Location winnerLobby = null; + public Location waitArea = null; + + public Game game = null; + + /** + * Creates a new Arena + * @param name Name of the arena + */ + public Arena(@NotNull String name) { + this.name = name; + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/game/EventListener.java b/src/main/java/com/MylesAndMore/Tumble/game/EventListener.java new file mode 100644 index 0000000..fd1b894 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/game/EventListener.java @@ -0,0 +1,253 @@ +package com.MylesAndMore.Tumble.game; + +import com.MylesAndMore.Tumble.config.SettingsManager; +import com.MylesAndMore.Tumble.plugin.GameState; +import com.MylesAndMore.Tumble.plugin.GameType; +import org.bukkit.Bukkit; +import org.bukkit.Effect; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.entity.Snowball; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockDropItemEvent; +import org.bukkit.event.block.LeavesDecayEvent; +import org.bukkit.event.entity.*; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.player.*; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +import static com.MylesAndMore.Tumble.Main.plugin; + +/** + * An event listener for a game of Tumble. + */ +public class EventListener implements Listener { + + private final static int ARENA_SIZE_SQ = 579; // The size of the arena squared (used for distance checks) + private final Game game; + + /** + * Create a new EventListener for a game. + * This should be active when the game starts (not while it is waiting) + * @param game The game that the EventListener belongs to. + */ + public EventListener(Game game) { + this.game = game; + } + + @EventHandler + public void PlayerJoinEvent(PlayerJoinEvent event) { + // Hide/show join message accordingly + if (event.getPlayer().getWorld() == game.arena.gameSpawn.getWorld() && SettingsManager.hideLeaveJoin) { + event.setJoinMessage(null); + } + } + + @EventHandler + public void PlayerQuitEvent(PlayerQuitEvent event) { + // Hide/show leave message accordingly + if (event.getPlayer().getWorld() == game.arena.gameSpawn.getWorld() && SettingsManager.hideLeaveJoin) { + event.setQuitMessage(null); + } + + // Remove player from game if they leave during a game + if (game.gamePlayers.contains(event.getPlayer())) { + game.removePlayer(event.getPlayer()); + } + } + + @EventHandler + public void PlayerDeathEvent(PlayerDeathEvent event) { + // Hide death messages if configured + if (event.getEntity().getWorld() == game.arena.gameSpawn.getWorld() && SettingsManager.hideDeathMessages) { + event.setDeathMessage(null); + } + + // Inform the game that the player died and respawn them + if (game.gamePlayers.contains(event.getEntity()) && game.gameState == GameState.RUNNING) { + game.playerDeath(event.getEntity()); + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> event.getEntity().spigot().respawn(), 10); + } + } + + @EventHandler + public void PlayerItemDamageEvent(PlayerItemDamageEvent event) { + // Remove item damage within games + if (game.gamePlayers.contains(event.getPlayer()) && game.gameState == GameState.RUNNING) { + event.setCancelled(true); + } + } + + @EventHandler + public void ProjectileLaunchEvent(ProjectileLaunchEvent event) { + if (!(event.getEntity().getShooter() instanceof Player p)) { return; } + if (!game.gamePlayers.contains(p)) { return; } + + if (game.roundType == GameType.SNOWBALLS && event.getEntity() instanceof Snowball) { + if (game.gameState == GameState.RUNNING) { + // Give players a snowball when they've used one (infinite snowballs) + Bukkit.getServer().getScheduler().runTask(plugin, () -> p.getInventory().addItem(new ItemStack(Material.SNOWBALL, 1))); + } else { + // Prevent projectiles (snowballs) from being thrown before the game starts + event.setCancelled(true); + } + } + } + + @EventHandler + public void ProjectileHitEvent(ProjectileHitEvent event) { + if (!(event.getEntity().getShooter() instanceof Player p)) { return; } + if (!game.gamePlayers.contains(p)) { return; } + + // Removes blocks that snowballs thrown by players have hit in the game world + if (game.roundType == GameType.SNOWBALLS && event.getEntity() instanceof Snowball) { + if (event.getHitBlock() != null) { + if (event.getHitBlock().getLocation().distanceSquared(game.arena.gameSpawn) < ARENA_SIZE_SQ) { + game.gamePlayers.forEach(pl -> pl.playEffect( + event.getHitBlock().getLocation(), + Effect.STEP_SOUND, + event.getHitBlock().getType())); + event.getHitBlock().setType(Material.AIR); + } + } else if (event.getHitEntity() != null) { + if (event.getHitEntity() instanceof Player hitPlayer) { + // Also cancel any knockback + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> hitPlayer.setVelocity(new Vector())); + } + } + } + } + + @EventHandler + public void PlayerDropItemEvent(PlayerDropItemEvent event) { + // Don't allow items to drop during the game + if (game.gamePlayers.contains(event.getPlayer())) { + event.setCancelled(true); + } + } + + @EventHandler + public void PlayerMoveEvent(PlayerMoveEvent event) { + if (!game.gamePlayers.contains(event.getPlayer())) { return; } + // Cancel movement if the game is starting (so players can't move before the game starts) + if (Objects.equals(game.gameState, GameState.STARTING) && !equalPosition(event.getFrom(),event.getTo())) { + event.setCancelled(true); + } + // Kill player if they are below configured Y level + if (game.arena.killAtY != null && game.gameState == GameState.RUNNING) { + if (event.getPlayer().getLocation().getY() <= game.arena.killAtY) { + event.getPlayer().setHealth(0); + } + } + } + + @EventHandler + public void BlockDropItemEvent(BlockDropItemEvent event) { + // If a block was going to drop an item (ex. snow dropping snowballs) in the game world, cancel it + if (event.getBlock().getWorld() == game.arena.gameSpawn.getWorld()) { + event.setCancelled(true); + } + } + + @EventHandler + public void PlayerInteractEvent(PlayerInteractEvent event) { + if (!game.gamePlayers.contains(event.getPlayer())) { return; } + + // Remove blocks when clicked in the game world (all game types require this functionality) + if (event.getAction() == Action.LEFT_CLICK_BLOCK && event.getClickedBlock() != null) { + event.getPlayer().playEffect( + event.getClickedBlock().getLocation(), + Effect.STEP_SOUND, + event.getClickedBlock().getType() + ); + event.getClickedBlock().setType(Material.AIR); + } + } + + @EventHandler + public void BlockBreakEvent(BlockBreakEvent event) { + // This just doesn't allow blocks to break in the gameWorld; the PlayerInteractEvent will take care of everything + // This prevents any weird client-server desync + if (game.gamePlayers.contains(event.getPlayer())) { + event.setCancelled(true); + } + } + + @EventHandler + public void FoodLevelChangeEvent(FoodLevelChangeEvent event) { + if (!(event.getEntity() instanceof Player p)) { return; } + if (!game.gamePlayers.contains(p)) { return; } + // INFINITE FOOD (YAY!!!!) + event.setCancelled(true); + } + + @EventHandler + public void EntityDamageEvent(EntityDamageEvent event) { + if (!(event.getEntity() instanceof Player p)) { return; } + if (!game.gamePlayers.contains(p)) { return; } + + // Check to see if a player got damaged by another entity (player, snowball, etc) in the gameWorld, if so, cancel it + if (event.getCause() == EntityDamageEvent.DamageCause.ENTITY_ATTACK + || event.getCause() == EntityDamageEvent.DamageCause.ENTITY_SWEEP_ATTACK + || event.getCause() == EntityDamageEvent.DamageCause.FALL) { + event.setCancelled(true); + } + } + + @EventHandler + public void EntityPickupItemEvent(EntityPickupItemEvent event) { + if (!(event.getEntity() instanceof Player p)) { return; } + if (!game.gamePlayers.contains(p)) { return; } + // Disable picking up items during the game + event.setCancelled(true); + } + + @EventHandler + public void InventoryDragEvent(InventoryDragEvent event) { + if (!(event.getWhoClicked() instanceof Player p)) { return; } + if (!game.gamePlayers.contains(p)) { return; } + // Disable inventory dragging + event.setCancelled(true); + } + + @EventHandler + public void PlayerRespawnEvent(PlayerRespawnEvent event) { + // Make sure players respawn in the correct location + if (game.gamePlayers.contains(event.getPlayer())) { + event.setRespawnLocation(game.arena.gameSpawn); + } + } + + @EventHandler + public void LeavesDecayEvent(LeavesDecayEvent event) { + if (event.getBlock().getWorld() != game.arena.gameSpawn.getWorld()) { return; } + // Prevent leaves from decaying in the game world (edge case moment) + if (event.getBlock().getLocation().distanceSquared(game.arena.gameSpawn) < ARENA_SIZE_SQ) { + event.setCancelled(true); + } + } + + /** + * Check to see if two locations are in the same place. + * A location also includes where the player is facing which is why this is used instead of .equals() + * @param l1 The first location + * @param l2 The second location + * @return True if they are in the same place + */ + public static boolean equalPosition(Location l1, @Nullable Location l2) { + if (l2 == null) { + return true; + } + return (l1.getX() == l2.getX()) && + (l1.getY() == l2.getY()) && + (l1.getZ() == l2.getZ()); + } +} \ No newline at end of file diff --git a/src/main/java/com/MylesAndMore/Tumble/game/Game.java b/src/main/java/com/MylesAndMore/Tumble/game/Game.java index 0ea74f5..078cd13 100644 --- a/src/main/java/com/MylesAndMore/Tumble/game/Game.java +++ b/src/main/java/com/MylesAndMore/Tumble/game/Game.java @@ -1,139 +1,286 @@ package com.MylesAndMore.Tumble.game; -import com.MylesAndMore.Tumble.plugin.Constants; - +import com.MylesAndMore.Tumble.config.SettingsManager; +import com.MylesAndMore.Tumble.config.LanguageManager; +import com.MylesAndMore.Tumble.plugin.GameState; +import com.MylesAndMore.Tumble.plugin.GameType; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.TextComponent; - import org.bukkit.*; import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Item; import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nullable; import java.util.*; +import static com.MylesAndMore.Tumble.Main.plugin; + /** * Everything relating to the Tumble game */ public class Game { - // Singleton class logic - private static Game gameInstance; - private Game() { - gameWorld = Bukkit.getWorld(Constants.getGameWorld()); - gameSpawn = Objects.requireNonNull(gameWorld).getSpawnLocation(); - } - public static Game getGame() { - if (gameInstance == null) { - gameInstance = new Game(); - } - return gameInstance; - } - // Define local game vars - private String gameState; - private String gameType; + public final GameType type; + public final Arena arena; + private final Location gameSpawn; + public final List gamePlayers = new ArrayList<>(); + private final HashMap gameWins = new HashMap<>(); + private final HashMap inventories = new HashMap<>(); + public GameState gameState = GameState.WAITING; + public GameType roundType; private int gameID = -1; private int autoStartID = -1; - private final World gameWorld; - private final Location gameSpawn; - private List gamePlayers; - private List roundPlayers; - private List gameWins; + private List playersAlive; + private EventListener eventListener; - private final Random Random = new Random(); + /** + * Create a new Game + * @param arena The arena the game is taking place in + * @param type The game type + */ + public Game(@NotNull Arena arena, @NotNull GameType type) { + this.arena = arena; + this.type = type; + this.gameSpawn = Objects.requireNonNull(arena.gameSpawn); + } /** - * Creates a new Game - * @param type The type of game - * @return true if the game succeeds creation, and false if not + * Adds a player to the wait area. Called from /tumble join + * Precondition: the game is in state WAITING + * @param p Player to add + * @return Whether the player was successfully added */ - public boolean startGame(@NotNull String type) { + public boolean addPlayer(Player p) { + if (gamePlayers.size() > 8) { return false; } + gamePlayers.add(p); + + if (arena.waitArea != null) { + inventories.put(p,p.getInventory().getContents()); + p.teleport(arena.waitArea); + p.getInventory().clear(); + } + if (gamePlayers.size() >= 2 && gameState == GameState.WAITING) { + autoStart(); + } else { + displayActionbar(Collections.singletonList(p), LanguageManager.fromKeyNoPrefix("waiting-for-players")); + } + return true; + } + + /** + * Starts the game + * Called from /tumble forceStart or after the wait counter finishes + */ + public void gameStart() { // Check if the game is starting or running - if (Objects.equals(gameState, "starting")) { return false; } - else if (Objects.equals(gameState, "running")) { return false; } - else { - // Define the gameType - switch (type) { - case "shovels", "snowballs", "mixed" -> { - gameState = "starting"; - // Set the type to gameType since it won't change for this mode - gameType = type; - // Clear the players' inventories so they can't bring any items into the game - clearInventories(Constants.getPlayersInLobby()); - // Generate the correct layers for a Shovels game - // The else statement is just in case the generator fails; this command will fail - if (generateLayers(type)) { - // Send all players from lobby to the game - scatterPlayers(Constants.getPlayersInLobby()); - } else { - return false; - } - } - default -> { - // The game type in the config did not match a specified game type - return false; + if (gameState != GameState.WAITING) { + return; + } + + // Cancel wait timer + Bukkit.getServer().getScheduler().cancelTask(autoStartID); + autoStartID = -1; + + // Register event listener + eventListener = new EventListener(this); + Bukkit.getServer().getPluginManager().registerEvents(eventListener, plugin); + + // Save inventories (if not already done) + for (Player p : gamePlayers) { + if (!inventories.containsKey(p)) { + inventories.put(p, p.getInventory().getContents()); + } + } + + // Give everyone full hunger and health + gamePlayers.forEach(p -> { + p.setHealth(20); + p.setFoodLevel(20); + }); + + roundStart(); + } + + /** + * Starts a round + */ + private void roundStart() { + gameState = GameState.STARTING; + playersAlive = new ArrayList<>(gamePlayers); + + scatterPlayers(gamePlayers); + // Put all players in spectator to prevent them from getting kicked for flying + setGamemode(gamePlayers, GameMode.SPECTATOR); + // Do it again in a bit in case they were not in the world yet + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> setGamemode(gamePlayers, GameMode.SPECTATOR), 10); + + clearInventories(gamePlayers); + clearArena(); + prepareGameType(type); + + // Begin the countdown sequence + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> countdown(() -> { + setGamemode(gamePlayers, GameMode.SURVIVAL); + gameState = GameState.RUNNING; + }), 100); + } + + /** + * Type specific setup: Generating layers and giving items + * @param type Game type + */ + private void prepareGameType(GameType type) { + roundType = type; + switch (type) { + case SHOVELS -> { + ItemStack shovel = new ItemStack(Material.IRON_SHOVEL); + shovel.addEnchantment(Enchantment.SILK_TOUCH, 1); + giveItems(gamePlayers, shovel); + + // Schedule a process to give snowballs after 2m30s (so people can't island, the OG game had this); + // Also add 160t because of the countdown + gameID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { + clearInventories(gamePlayers); + giveItems(gamePlayers, new ItemStack(Material.SNOWBALL)); + roundType = GameType.SNOWBALLS; + displayActionbar(gamePlayers, LanguageManager.fromKeyNoPrefix("showdown")); + playSound(gamePlayers, Sound.ENTITY_ELDER_GUARDIAN_CURSE, SoundCategory.HOSTILE, 1, 1); + + // End the round in another 2m30s + gameID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, this::roundEnd, 3000); + }, 3160); + } + case SNOWBALLS -> { + giveItems(gamePlayers, new ItemStack(Material.SNOWBALL)); + + // End the round in 5m + gameID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, this::roundEnd, 6160); + } + case MIXED -> { + Random random = new Random(); + switch (random.nextInt(2)) { + case 0 -> prepareGameType(GameType.SHOVELS); + case 1 -> prepareGameType(GameType.SNOWBALLS); } + return; } - // Update the game/round players for later - gamePlayers = new ArrayList<>(Constants.getPlayersInGame()); - roundPlayers = new ArrayList<>(Constants.getPlayersInGame()); - // Create a list that will later keep track of each player's wins - gameWins = new ArrayList<>(); - gameWins.addAll(List.of(0,0,0,0,0,0,0,0)); - // Put all players in spectator to prevent them from getting kicked for flying (this needs a delay bc servers are slow) - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> setGamemode(gamePlayers, GameMode.SPECTATOR), 25); - // Wait 5s (100t) for the clients to load in - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - // Begin the countdown sequence - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); - displayTitles(gamePlayers, ChatColor.DARK_GREEN + "3", null, 3, 10, 7); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); - displayTitles(gamePlayers, ChatColor.YELLOW + "2", null, 3, 10, 7); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); - displayTitles(gamePlayers, ChatColor.DARK_RED + "1", null, 3, 10, 7); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 2); - displayTitles(gamePlayers, ChatColor.GREEN + "Go!", null, 1, 5, 1); - setGamemode(gamePlayers, GameMode.SURVIVAL); - gameState = "running"; - }, 20); - }, 20); - }, 20); - }, 100); } - return true; + Generator.generateLayers(gameSpawn, type); } /** - * Initiates an automatic start of a Tumble game + * Ends round: Finds and displays winner, starts next round if necessary */ - public void autoStart() { - // Wait for the player to load in - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - gameState = "waiting"; - displayActionbar(Constants.getPlayersInLobby(), ChatColor.GREEN + "Game will begin in 15 seconds!"); - playSound(Constants.getPlayersInLobby(), Sound.BLOCK_NOTE_BLOCK_CHIME, SoundCategory.BLOCKS, 1, 1); - Constants.getMVWorldManager().loadWorld(Constants.getGameWorld()); - // Schedule a process to start the game in 300t (15s) and save the PID so we can cancel it later if needed - autoStartID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> startGame(Constants.getGameType()), 300); - }, 50); + private void roundEnd() { + // Cancel the tasks that auto-end the round + gameState = GameState.ENDING; + Bukkit.getServer().getScheduler().cancelTask(gameID); + gameID = -1; + + playSound(gamePlayers, Sound.BLOCK_NOTE_BLOCK_PLING, SoundCategory.BLOCKS, 5, 0); + + // Check if there was a definite winner or not + if (playersAlive.size() == 1) { + Player winner = playersAlive.get(0); + // Set the wins of the player to their current # of wins + 1 + if (!gameWins.containsKey(winner)) { + gameWins.put(winner, 0); + } + gameWins.put(winner, gameWins.get(winner)+1); + + if (gameWins.get(winner) == 3) { + gameEnd(); + } else { // If that player doesn't have three wins, nobody else does, so we need another round + displayTitles(gamePlayers, LanguageManager.fromKeyNoPrefix("round-over"), LanguageManager.fromKeyNoPrefix("round-winner").replace("%winner%", winner.getDisplayName()), 5, 60, 5); + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, this::roundStart, 100); + } + } else { + displayTitles(gamePlayers, LanguageManager.fromKeyNoPrefix("round-over"), LanguageManager.fromKeyNoPrefix("round-draw"), 5, 60, 5); + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, this::roundStart, 100); + } } /** - * Cancels a "waiting" automatic start + * Ends game: Displays overall winner and teleports players to lobby */ - public void cancelStart() { - Bukkit.getServer().getScheduler().cancelTask(Game.getGame().getAutoStartID()); - displayActionbar(Constants.getPlayersInLobby(), ChatColor.RED + "Game start cancelled!"); - playSound(Constants.getPlayersInLobby(), Sound.BLOCK_NOTE_BLOCK_BASS, SoundCategory.BLOCKS, 1, 1); - gameState = null; - autoStartID = -1; + private void gameEnd() { + if (!gamePlayers.isEmpty()) { + + setGamemode(gamePlayers, GameMode.SPECTATOR); + clearInventories(gamePlayers); + + // Display winner + Player winner = getPlayerWithMostWins(gameWins); + if (winner != null) { + displayTitles(gamePlayers, LanguageManager.fromKeyNoPrefix("game-over"), LanguageManager.fromKeyNoPrefix("game-winner").replace("%winner%",winner.getDisplayName()), 5, 60, 5); + } + + displayActionbar(gamePlayers, LanguageManager.fromKeyNoPrefix("lobby-in-10")); + // Wait 10s (200t), then clear the arena and teleport players back + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { + clearArena(); + for (Player p : gamePlayers) { + sendToLobby(p, p == winner); + } + }, 200); + } + + cleanup(false); + } + + /** + * Stops the game, usually while it is still going + * Called if too many players leave, or from /tumble forceStop + */ + public void stopGame() { + // A new list must be created to avoid removing elements while iterating + List players = new ArrayList<>(gamePlayers); + players.forEach(this::removePlayer); + clearArena(); + cleanup(true); + } + + /** + * Removes a player from the game. + * Called when a player leaves the server, or if they issue the leave command + * @param p Player to remove + */ + public void removePlayer(Player p) { + gamePlayers.remove(p); + + // Check if the game has not started yet + if (gameState == GameState.WAITING) { + // Inform player that there are no longer enough players to start + if (gamePlayers.size() < 2) { + displayActionbar(gamePlayers, LanguageManager.fromKeyNoPrefix("waiting-for-players")); + } + + sendToLobby(p, false); + } else { + // Stop the game if there are no longer enough players + if (gamePlayers.size() < 2) { + stopGame(); + } + + sendToLobby(p, false); // You can never win if you quit, remember that kids!! + } + } + + /** + * Attempts to initiate an automatic start of a Tumble game + */ + public void autoStart() { + int waitDuration = SettingsManager.waitDuration; + if (waitDuration <= 0) { return; } + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { + displayActionbar(gamePlayers, LanguageManager.fromKeyNoPrefix("time-till-start").replace("%wait%",waitDuration+"")); + playSound(gamePlayers, Sound.BLOCK_NOTE_BLOCK_CHIME, SoundCategory.BLOCKS, 1, 1); + // Schedule a process to start the game in the specified waitDuration and save the PID so we can cancel it later if needed + autoStartID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, this::gameStart, waitDuration * 20L); + }, 50); } /** @@ -142,190 +289,77 @@ public void cancelStart() { */ public void playerDeath(Player player) { player.setGameMode(GameMode.SPECTATOR); - // Add a delay to tp them to the gameWorld just in case they have a bed in another world (yes you Jacob) - // Delay is needed because instant respawn is a lie (it's not actually instant) - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - player.teleport(gameSpawn); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> player.setGameMode(GameMode.SPECTATOR), 5); - }, 5); - // remove that player (who just died) from the roundPlayersArray, effectively eliminating them, - roundPlayers.remove(player); - // If there are less than 2 players in the game (1 just died), - if (roundPlayers.size() < 2) { - roundEnd(roundPlayers.get(0)); + // Remove that player (who just died) from the alive players, effectively eliminating them + playersAlive.remove(player); + // If there are less than 2 players in the game (1 just died), end the round + if (playersAlive.size() < 2 && gameState == GameState.RUNNING) { + roundEnd(); } } - // Methods to get the game type and game state for other classes outside the Game + // -- Utility functions -- /** - * @return The game's current state as a String ("waiting", "starting", "running", "complete") - * Can also be null if not initialized. + * Teleports a list of players to the specified scatter locations in the gameWorld + * @param players a List of Players to teleport */ - public String getGameState() { return gameState; } + private void scatterPlayers(List players) { + double x = gameSpawn.getX(); + double y = gameSpawn.getY(); + double z = gameSpawn.getZ(); + World gameWorld = gameSpawn.getWorld(); + // Create the scatter locations based off the game's spawn + List scatterLocations = new ArrayList<>(List.of( + new Location(gameWorld, (x - 14.5), y, (z + 0.5), -90, 0), + new Location(gameWorld, (x + 0.5), y, (z - 14.5), 0, 0), + new Location(gameWorld, (x + 15.5), y, (z + 0.5), 90, 0), + new Location(gameWorld, (x + 0.5), y, (z + 15.5), 180, 0), + new Location(gameWorld, (x - 10.5), y, (z - 10.5), -45, 0), + new Location(gameWorld, (x - 10.5), y, (z + 11.5), -135, 0), + new Location(gameWorld, (x + 11.5), y, (z - 10.5), 45, 0), + new Location(gameWorld, (x + 11.5), y, (z + 11.5), 135, 0))); + Collections.shuffle(scatterLocations); + for (Player aPlayer : players) { + aPlayer.teleport(scatterLocations.get(0)); + scatterLocations.remove(0); // Remove that location so multiple players won't get the same one + } + } /** - * @return The Bukkit process ID of the autostart process, if applicable - * Can also be null if not initialized, or -1 if the process failed to schedule. + * Displays the 3, 2, 1 countdown + * @param doAfter Will be executed after the countdown */ - public int getAutoStartID() { return autoStartID; } - + private void countdown(Runnable doAfter) { + playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); + displayTitles(gamePlayers, LanguageManager.fromKeyNoPrefix("count-3"), null, 3, 10, 7); + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { + playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); + displayTitles(gamePlayers, LanguageManager.fromKeyNoPrefix("count-2"), null, 3, 10, 7); + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { + playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); + displayTitles(gamePlayers, LanguageManager.fromKeyNoPrefix("count-1"), null, 3, 10, 7); + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { + playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 2); + displayTitles(gamePlayers, LanguageManager.fromKeyNoPrefix("count-go"), null, 1, 5, 1); + doAfter.run(); + }, 20); + }, 20); + }, 20); + } - private final Layers layers = new Layers(); /** - * Generates the layers in the gameWorld for a certain gameType - * @param type can be either "shovels", "snowballs", or "mixed", anything else will fail generation - * @return true if gameType was recognized and layers were (hopefully) generated, false if unrecognized + * Finds the player with the most wins + * @param list List of players and their number of wins + * @return Player with the most wins */ - private boolean generateLayers(String type) { - // Create a new Location for the layers to work with--this is so that we don't modify the actual gameSpawn var - Location layer = new Location(gameSpawn.getWorld(), gameSpawn.getX(), gameSpawn.getY(), gameSpawn.getZ(), gameSpawn.getYaw(), gameSpawn.getPitch()); - if (Objects.equals(type, "shovels")) { - layer.setY(layer.getY() - 1); - // Choose a random type of generation; a circular layer, a square layer, or a multi-tiered layer of either variety - if (Random.nextInt(4) == 0) { - // Circular layer - Generator.generateClumps(Generator.generateLayer(layer, 17, 1, Material.SNOW_BLOCK), layers.getSafeMaterialList()); - } - else if (Random.nextInt(4) == 1) { - // Square layer - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 17, layer.getY(), layer.getZ() - 17), new Location(layer.getWorld(), layer.getX() + 17, layer.getY(), layer.getZ() + 17), Material.SNOW_BLOCK), layers.getSafeMaterialList()); - } - else if (Random.nextInt(4) == 2) { - // Multi-tiered circle - Generator.generateClumps(Generator.generateLayer(layer, 17, 1, Material.SNOW_BLOCK), layers.getSafeMaterialList()); - Generator.generateLayer(layer, 13, 1, Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateLayer(layer, 13, 1, Material.GRASS_BLOCK), layers.getMaterialList()); - Generator.generateLayer(layer, 4, 1, Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateLayer(layer, 4, 1, Material.PODZOL), layers.getMaterialList()); - } - else { - // Multi-tiered square - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 17, layer.getY(), layer.getZ() - 17), new Location(layer.getWorld(), layer.getX() + 17, layer.getY(), layer.getZ() + 17), Material.SNOW_BLOCK), layers.getSafeMaterialList()); - Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 13, layer.getY(), layer.getZ() - 13), new Location(layer.getWorld(), layer.getX() + 13, layer.getY(), layer.getZ() + 13), Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 13, layer.getY(), layer.getZ() - 13), new Location(layer.getWorld(), layer.getX() + 13, layer.getY(), layer.getZ() + 13), Material.GRASS_BLOCK), layers.getMaterialList()); - Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 7, layer.getY(), layer.getZ() - 7), new Location(layer.getWorld(), layer.getX() + 7, layer.getY(), layer.getZ() + 7), Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 7, layer.getY(), layer.getZ() - 7), new Location(layer.getWorld(), layer.getX() + 7, layer.getY(), layer.getZ() + 7), Material.PODZOL), layers.getMaterialList()); - } - ItemStack shovel = new ItemStack(Material.IRON_SHOVEL); - shovel.addEnchantment(Enchantment.SILK_TOUCH, 1); - if (Objects.equals(gameState, "running")) { - giveItems(Constants.getPlayersInGame(), shovel); - } - else if (Objects.equals(gameState, "starting")) { - giveItems(Constants.getPlayersInLobby(), shovel); - } - // Schedule a process to give snowballs after 2m30s (so people can't island, the OG game had this); add 160t because of the countdown - gameID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - clearInventories(gamePlayers); - giveItems(gamePlayers, new ItemStack(Material.SNOWBALL)); - displayActionbar(gamePlayers, ChatColor.DARK_RED + "Showdown!"); - playSound(gamePlayers, Sound.ENTITY_ELDER_GUARDIAN_CURSE, SoundCategory.HOSTILE, 1, 1); - // End the round in another 2m30s - gameID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> roundEnd(null), 3000); - }, 3160); - } - else if (Objects.equals(type, "snowballs")) { - layer.setY(layer.getY() - 1); - // Similar generation to shovels, except there are three layers - if (Random.nextInt(4) == 0) { - // Circular layer - Generator.generateClumps(Generator.generateLayer(layer, 17, 1, Material.STONE), layers.getSafeMaterialList()); - layer.setY(layer.getY() - 6); - Generator.generateClumps(Generator.generateLayer(layer, 17, 1, Material.STONE), layers.getMaterialList()); - layer.setY(layer.getY() - 6); - Generator.generateClumps(Generator.generateLayer(layer, 17, 1, Material.STONE), layers.getMaterialList()); - } - else if (Random.nextInt(4) == 1) { - // Square layer - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 17, layer.getY(), layer.getZ() - 17), new Location(layer.getWorld(), layer.getX() + 17, layer.getY(), layer.getZ() + 17), Material.STONE), layers.getSafeMaterialList()); - layer.setY(layer.getY() - 6); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 17, layer.getY(), layer.getZ() - 17), new Location(layer.getWorld(), layer.getX() + 17, layer.getY(), layer.getZ() + 17), Material.STONE), layers.getMaterialList()); - layer.setY(layer.getY() - 6); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 17, layer.getY(), layer.getZ() - 17), new Location(layer.getWorld(), layer.getX() + 17, layer.getY(), layer.getZ() + 17), Material.STONE), layers.getMaterialList()); - } - else if (Random.nextInt(4) == 2) { - // Multi-tiered circle - Generator.generateClumps(Generator.generateLayer(layer, 17, 1, Material.STONE), layers.getSafeMaterialList()); - Generator.generateLayer(layer, 13, 1, Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateLayer(layer, 13, 1, Material.GRANITE), layers.getMaterialList()); - Generator.generateLayer(layer, 4, 1, Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateLayer(layer, 4, 1, Material.LIME_GLAZED_TERRACOTTA), layers.getMaterialList()); - layer.setY(layer.getY() - 6); - - Generator.generateClumps(Generator.generateLayer(layer, 17, 1, Material.STONE), layers.getSafeMaterialList()); - Generator.generateLayer(layer, 13, 1, Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateLayer(layer, 13, 1, Material.GRANITE), layers.getMaterialList()); - Generator.generateLayer(layer, 4, 1, Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateLayer(layer, 4, 1, Material.LIME_GLAZED_TERRACOTTA), layers.getMaterialList()); - layer.setY(layer.getY() - 6); - - Generator.generateClumps(Generator.generateLayer(layer, 17, 1, Material.STONE), layers.getSafeMaterialList()); - Generator.generateLayer(layer, 13, 1, Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateLayer(layer, 13, 1, Material.GRANITE), layers.getMaterialList()); - Generator.generateLayer(layer, 4, 1, Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateLayer(layer, 4, 1, Material.LIME_GLAZED_TERRACOTTA), layers.getMaterialList()); - } - else { - // Multi-tiered square - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 17, layer.getY(), layer.getZ() - 17), new Location(layer.getWorld(), layer.getX() + 17, layer.getY(), layer.getZ() + 17), Material.STONE), layers.getSafeMaterialList()); - Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 13, layer.getY(), layer.getZ() - 13), new Location(layer.getWorld(), layer.getX() + 13, layer.getY(), layer.getZ() + 13), Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 13, layer.getY(), layer.getZ() - 13), new Location(layer.getWorld(), layer.getX() + 13, layer.getY(), layer.getZ() + 13), Material.GRANITE), layers.getMaterialList()); - Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 7, layer.getY(), layer.getZ() - 7), new Location(layer.getWorld(), layer.getX() + 7, layer.getY(), layer.getZ() + 7), Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 7, layer.getY(), layer.getZ() - 7), new Location(layer.getWorld(), layer.getX() + 7, layer.getY(), layer.getZ() + 7), Material.LIME_GLAZED_TERRACOTTA), layers.getMaterialList()); - layer.setY(layer.getY() - 6); - - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 17, layer.getY(), layer.getZ() - 17), new Location(layer.getWorld(), layer.getX() + 17, layer.getY(), layer.getZ() + 17), Material.STONE), layers.getSafeMaterialList()); - Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 13, layer.getY(), layer.getZ() - 13), new Location(layer.getWorld(), layer.getX() + 13, layer.getY(), layer.getZ() + 13), Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 13, layer.getY(), layer.getZ() - 13), new Location(layer.getWorld(), layer.getX() + 13, layer.getY(), layer.getZ() + 13), Material.GRANITE), layers.getMaterialList()); - Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 7, layer.getY(), layer.getZ() - 7), new Location(layer.getWorld(), layer.getX() + 7, layer.getY(), layer.getZ() + 7), Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 7, layer.getY(), layer.getZ() - 7), new Location(layer.getWorld(), layer.getX() + 7, layer.getY(), layer.getZ() + 7), Material.LIME_GLAZED_TERRACOTTA), layers.getMaterialList()); - layer.setY(layer.getY() - 6); - - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 17, layer.getY(), layer.getZ() - 17), new Location(layer.getWorld(), layer.getX() + 17, layer.getY(), layer.getZ() + 17), Material.STONE), layers.getSafeMaterialList()); - Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 13, layer.getY(), layer.getZ() - 13), new Location(layer.getWorld(), layer.getX() + 13, layer.getY(), layer.getZ() + 13), Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 13, layer.getY(), layer.getZ() - 13), new Location(layer.getWorld(), layer.getX() + 13, layer.getY(), layer.getZ() + 13), Material.GRANITE), layers.getMaterialList()); - Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 7, layer.getY(), layer.getZ() - 7), new Location(layer.getWorld(), layer.getX() + 7, layer.getY(), layer.getZ() + 7), Material.AIR); - layer.setY(layer.getY() - 1); - Generator.generateClumps(Generator.generateCuboid(new Location(layer.getWorld(), layer.getX() - 7, layer.getY(), layer.getZ() - 7), new Location(layer.getWorld(), layer.getX() + 7, layer.getY(), layer.getZ() + 7), Material.LIME_GLAZED_TERRACOTTA), layers.getMaterialList()); - } - if (Objects.equals(gameState, "running")) { - giveItems(Constants.getPlayersInGame(), new ItemStack(Material.SNOWBALL)); - } - else if (Objects.equals(gameState, "starting")) { - giveItems(Constants.getPlayersInLobby(), new ItemStack(Material.SNOWBALL)); - } - // End the round in 5m - gameID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> roundEnd(null), 6160); - } - else if (Objects.equals(type, "mixed")) { - // Randomly select either shovels or snowballs and re-run the method - if (Random.nextInt(2) == 0) { - generateLayers("shovels"); - } else { - generateLayers("snowballs"); + private Player getPlayerWithMostWins(HashMap list) { + Player largest = null; + for (Player p: list.keySet()) { + if (largest == null || list.get(p) > list.get(largest)) { + largest = p; } } - // Game type was invalid - else { - return false; - } - return true; + return largest; } /** @@ -395,145 +429,58 @@ private void displayActionbar(List players, String message) { * @param pitch The pitch of the sound */ private void playSound(@NotNull List players, @NotNull Sound sound, @NotNull SoundCategory category, float volume, float pitch) { - for (Player aPlayer : players) { - aPlayer.playSound(aPlayer, sound, category, volume, pitch); - } + players.forEach(player -> player.playSound(player.getLocation(), sound, category, volume, pitch)); } /** - * Teleports a list of players to the specified scatter locations in the gameWorld - * @param players a List of Players to teleport + * Clears old layers + * (as a fill command, this would be /fill ~-20 ~-20 ~-20 ~20 ~ ~20 relative to spawn) */ - private void scatterPlayers(List players) { - double x = gameSpawn.getX(); - double y = gameSpawn.getY(); - double z = gameSpawn.getZ(); - // Create the scatter locations based off the game's spawn - List scatterLocations = new ArrayList<>(List.of( - new Location(gameWorld, (x - 14.5), y, (z + 0.5), -90, 0), - new Location(gameWorld, (x + 0.5), y, (z - 14.5), 0, 0), - new Location(gameWorld, (x + 15.5), y, (z + 0.5), 90, 0), - new Location(gameWorld, (x + 0.5), y, (z + 15.5), 180, 0), - new Location(gameWorld, (x - 10.5), y, (z - 10.5), -45, 0), - new Location(gameWorld, (x - 10.5), y, (z + 11.5), -135, 0), - new Location(gameWorld, (x + 11.5), y, (z - 10.5), 45, 0), - new Location(gameWorld, (x + 11.5), y, (z + 11.5), 135, 0))); - Collections.shuffle(scatterLocations); - for (Player aPlayer : players) { - aPlayer.teleport(scatterLocations.get(0)); - scatterLocations.remove(0); // Remove that location so multiple players won't get the same one - } + private void clearArena() { + Generator.generateCuboid( + new Location(gameSpawn.getWorld(), gameSpawn.getX() - 20, gameSpawn.getY() - 20, gameSpawn.getZ() - 20), + new Location(gameSpawn.getWorld(), gameSpawn.getX() + 20, gameSpawn.getY(), gameSpawn.getZ() + 20), + Material.AIR); } - private void roundEnd(@Nullable Player winner) { - // Cancel the tasks that auto-end the round - Bukkit.getServer().getScheduler().cancelTask(gameID); - // Clear old layers (as a fill command, this would be /fill ~-20 ~-20 ~-20 ~20 ~ ~20 relative to spawn) - Generator.generateCuboid(new Location(gameSpawn.getWorld(), gameSpawn.getX() - 20, gameSpawn.getY() - 20, gameSpawn.getZ() - 20), new Location(gameSpawn.getWorld(), gameSpawn.getX() + 20, gameSpawn.getY(), gameSpawn.getZ() + 20), Material.AIR); - playSound(gamePlayers, Sound.BLOCK_NOTE_BLOCK_PLING, SoundCategory.BLOCKS, 5, 0); - // Check if there was a definite winner or not - if (winner != null) { - // Set the wins of the player to their current # of wins + 1 - gameWins.set(gamePlayers.indexOf(winner), (gameWins.get(gamePlayers.indexOf(winner)) + 1)); - // If the player has three wins, they won the game, so initiate the gameEnd - if (gameWins.get(gamePlayers.indexOf(winner)) == 3) { - gameEnd(winner); - } - // If that player doesn't have three wins, nobody else does, so we need another round - else { - roundPlayers.get(0).setGameMode(GameMode.SPECTATOR); - roundPlayers.remove(0); - roundPlayers.addAll(gamePlayers); - clearInventories(gamePlayers); - displayTitles(gamePlayers, ChatColor.RED + "Round over!", ChatColor.GOLD + winner.getName() + " has won the round!", 5, 60, 5); - // Wait for the player to respawn before completely lagging the server ._. - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - generateLayers(gameType); - // Wait 5s (100t) for tp method - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - // Kill all items (pistons are weird) - for (Entity entity : gameWorld.getEntities()) { - if (entity instanceof Item) { - entity.remove(); - } - } - gameState = "starting"; - scatterPlayers(gamePlayers); - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); - displayTitles(gamePlayers, ChatColor.DARK_GREEN + "3", null, 3, 10, 7); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); - displayTitles(gamePlayers, ChatColor.YELLOW + "2", null, 3, 10, 7); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); - displayTitles(gamePlayers, ChatColor.DARK_RED + "1", null, 3, 10, 7); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 2); - displayTitles(gamePlayers, ChatColor.GREEN + "Go!", null, 1, 5, 1); - setGamemode(gamePlayers, GameMode.SURVIVAL); - gameState = "running"; - }, 20); - }, 20); - }, 20); - }, 100); - }, 1); + /** + * Teleports a player to the lobby and restores their inventory + * @param p Player to teleport + * @param winner Whether the player is the winner + */ + private void sendToLobby(Player p, boolean winner) { + p.getInventory().clear(); + p.setGameMode(GameMode.SURVIVAL); + if (winner && arena.winnerLobby != null) { + p.teleport(arena.winnerLobby); + } else { + // Use default world spawn if lobby is not set + if (arena.lobby == null) { + p.teleport(Objects.requireNonNull(Bukkit.getWorlds().get(0)).getSpawnLocation()); + } else { + p.teleport(Objects.requireNonNull(arena.lobby)); } } - else { - setGamemode(gamePlayers, GameMode.SPECTATOR); - roundPlayers.clear(); - roundPlayers.addAll(gamePlayers); - clearInventories(gamePlayers); - displayTitles(gamePlayers, ChatColor.RED + "Round over!", ChatColor.GOLD + "Draw!", 5, 60, 5); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - generateLayers(gameType); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - for (Entity entity : gameWorld.getEntities()) { - if (entity instanceof Item) { - entity.remove(); - } - } - gameState = "starting"; - scatterPlayers(gamePlayers); - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); - displayTitles(gamePlayers, ChatColor.DARK_GREEN + "3", null, 3, 10, 7); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); - displayTitles(gamePlayers, ChatColor.YELLOW + "2", null, 3, 10, 7); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 1); - displayTitles(gamePlayers, ChatColor.DARK_RED + "1", null, 3, 10, 7); - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - playSound(gamePlayers, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.NEUTRAL, 5, 2); - displayTitles(gamePlayers, ChatColor.GREEN + "Go!", null, 1, 5, 1); - setGamemode(gamePlayers, GameMode.SURVIVAL); - gameState = "running"; - }, 20); - }, 20); - }, 20); - }, 100); - }, 1); + if (inventories.containsKey(p)) { + p.getInventory().setContents(inventories.get(p)); } } - private void gameEnd(Player winner) { - winner.setGameMode(GameMode.SPECTATOR); - clearInventories(gamePlayers); - displayTitles(gamePlayers, ChatColor.RED + "Game over!", ChatColor.GOLD + winner.getName() + " has won the game!", 5, 60, 5); - displayActionbar(gamePlayers, ChatColor.BLUE + "Returning to lobby in ten seconds..."); - // Wait 10s (200t), then - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> { - // First, check to see if there is a separate location to tp the winner to - if ((Constants.getPlugin().getConfig().getDouble("winnerTeleport.x") != 0) && (Constants.getPlugin().getConfig().getDouble("winnerTeleport.y") != 0) && (Constants.getPlugin().getConfig().getDouble("winnerTeleport.z") != 0)) { - winner.teleport(new Location(Bukkit.getWorld(Constants.getLobbyWorld()), Constants.getPlugin().getConfig().getDouble("winnerTeleport.x"), Constants.getPlugin().getConfig().getDouble("winnerTeleport.y"), Constants.getPlugin().getConfig().getDouble("winnerTeleport.z"))); - // Remove the winner from the gamePlayers so they don't get double-tp'd - gamePlayers.remove(winner); - } - // Send all players back to lobby (spawn) - for (Player aPlayer : gamePlayers) { - aPlayer.teleport(Objects.requireNonNull(Bukkit.getWorld(Constants.getLobbyWorld())).getSpawnLocation()); - } - }, 200); - gameState = "complete"; + /** + * Cleans up the game's server resources + * @param fast Whether to clean up quickly (avoid a graceful cleanup) + */ + private void cleanup(boolean fast) { + Bukkit.getServer().getScheduler().cancelTask(gameID); + gameID = -1; + Bukkit.getServer().getScheduler().cancelTask(autoStartID); + autoStartID = -1; + arena.game = null; + if (fast) { + HandlerList.unregisterAll(eventListener); + } else { + // Delay the unregistering of the event listener to prevent issues like players respawning in the wrong location, etc. + Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> HandlerList.unregisterAll(eventListener), 20); + } } } diff --git a/src/main/java/com/MylesAndMore/Tumble/game/Generator.java b/src/main/java/com/MylesAndMore/Tumble/game/Generator.java index ecaa1b7..0837edf 100644 --- a/src/main/java/com/MylesAndMore/Tumble/game/Generator.java +++ b/src/main/java/com/MylesAndMore/Tumble/game/Generator.java @@ -1,35 +1,63 @@ package com.MylesAndMore.Tumble.game; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; +import com.MylesAndMore.Tumble.config.LayerManager; +import com.MylesAndMore.Tumble.plugin.GameType; +import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; import java.util.*; /** - * Holds the methods that generate blocks in-game such as cylinders, cuboids, and block clumps. + * The Generator can generate basic shapes and layers for the game */ public class Generator { + + private static final int CIRCLE_RADIUS = 17; + private static final int SQUARE_RADIUS = 17; + private static final int MULTI_TIER_RADIUS1 = 17; + private static final int MULTI_TIER_RADIUS2 = 13; + private static final int MULTI_TIER_RADIUS3_CIRCULAR = 4; + private static final int MULTI_TIER_RADIUS3_SQUARE = 7; + private static final int LAYER_DROP_HEIGHT = 6; // How far down the next layer should be generated in multi-layer generation + + /** + * Generates layers for a round + * @param center The center of the layers + * @param type The type of the round (either shovels or snowballs) + */ + public static void generateLayers(Location center, GameType type) { + if (type == GameType.MIXED) { return; } // Cannot infer generation type from mixed + Random random = new Random(); + Location layer = center.clone(); + // The only difference between shovel and snowball generation is the amount of layers + int numLayers = type == GameType.SNOWBALLS ? 3 : 1; + // Move down one block before generating + layer.setY(layer.getY() - 1); + switch (random.nextInt(4)) { + case 0 -> generateCircularLayers(layer, new int[]{CIRCLE_RADIUS}, numLayers); // Single circular layer + case 1 -> generateSquareLayers(layer, new int[]{SQUARE_RADIUS}, numLayers); // Single square layer + case 2 -> generateCircularLayers(layer, new int[]{MULTI_TIER_RADIUS1, MULTI_TIER_RADIUS2, MULTI_TIER_RADIUS3_CIRCULAR}, numLayers); // Multi-tiered circular layer + case 3 -> generateSquareLayers(layer, new int[]{MULTI_TIER_RADIUS1, MULTI_TIER_RADIUS2, MULTI_TIER_RADIUS3_SQUARE}, numLayers); // Multi-tiered square layer + } + } + /** - * Generates a layer (basically just a cylinder) as good as possible with blocks + * Generates a cylinder * @param center The center of the layer (Location) - * @param radius The whole number radius of the circle - * @param height The whole number height of the circle (1 for a flat layer) + * @param radius The radius of the layer + * @param height The height of the layer (1 for a flat layer) * @param material The Material to use for generation - * - * @return A list of Blocks containing all the blocks it just changed + * @return A list containing all changed blocks */ - public static List generateLayer(Location center, int radius, int height, Material material) { + public static List generateCylinder(Location center, int radius, int height, Material material) { int Cx = center.getBlockX(); int Cy = center.getBlockY(); int Cz = center.getBlockZ(); + int rSq = radius * radius; World world = center.getWorld(); List blocks = new ArrayList<>(); - int rSq = radius * radius; - for (int y = Cy; y < Cy + height; y++) { for (int x = Cx - radius; x <= Cx + radius; x++) { for (int z = Cz - radius; z <= Cz + radius; z++) { @@ -44,10 +72,11 @@ public static List generateLayer(Location center, int radius, int height, } /** - * Generates a cuboid (literally just a ripoff fill command) - * @param firstPos The first Location to fill (first three coords in a fill command) + * Generates a cuboid + * @param firstPos The first Location to fill from (first three coords in a fill command) * @param secondPos The second Location to fill to (second three coords) * @param material The Material to fill + * @return A list containing all changed blocks */ public static List generateCuboid(Location firstPos, Location secondPos, Material material) { World world = firstPos.getWorld(); @@ -71,40 +100,124 @@ public static List generateCuboid(Location firstPos, Location secondPos, } /** - * Generates clumps in a pre-generated layer. - * @param blockList A list of block Locations that this method is allowed to edit + * Generates clumps in a pre-generated layer + * @param blockList A list of Blocks that this method is allowed to edit * @param materialList A list of Materials for the generator to randomly choose from. * Keep in mind that not all Materials may be used, the amount used depends on the size of the layer. * More Materials = more randomization */ - public static void generateClumps(List blockList, List materialList) { + private static void generateClumps(List blockList, List materialList) { Random random = new Random(); - // Make new lists so we can manipulate them List blocks = new ArrayList<>(blockList); List materials = new ArrayList<>(materialList); Collections.shuffle(materials); - while (blocks.size() > 0) { + + while (!blocks.isEmpty()) { Material randomMaterial = materials.get(random.nextInt(materials.size())); - Block aBlock = blocks.get(0); - aBlock.setType(randomMaterial); - // Get the blocks around that and change it to that same material (this is the basis of "clumps") - if (blocks.contains(aBlock.getRelative(BlockFace.NORTH))) { - aBlock.getRelative(BlockFace.NORTH).setType(randomMaterial); - blocks.remove(aBlock.getRelative(BlockFace.NORTH)); + Block block = blocks.get(random.nextInt(blocks.size())); + block.setType(randomMaterial); + List modifiedBlocks = setRelativeBlocks(blocks, block); + blocks.removeAll(modifiedBlocks); + // There is a 50% (then 25%, 12.5%, ...) chance to continue modifying blocks aka growing the clump + double probability = 0.5; + while (!modifiedBlocks.isEmpty() && random.nextDouble() < probability) { + Block nextBlock = modifiedBlocks.get(random.nextInt(modifiedBlocks.size())); + nextBlock.setType(randomMaterial); + modifiedBlocks = setRelativeBlocks(blocks, nextBlock); + blocks.removeAll(modifiedBlocks); + probability /= 2; } - if (blocks.contains(aBlock.getRelative(BlockFace.SOUTH))) { - aBlock.getRelative(BlockFace.SOUTH).setType(randomMaterial); - blocks.remove(aBlock.getRelative(BlockFace.SOUTH)); + } + } + + /** + * Sets all Blocks adjacent to `block` in `blocks` to the same Material as `block` + * @param blocks The list of blocks to modify + * @param block The reference block + * @return A list of all modified blocks, including `block` + */ + private static List setRelativeBlocks(List blocks, Block block) { + List modifiedBlocks = new ArrayList<>(); + BlockFace[] faces = {BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST}; + for (BlockFace face : faces) { + Block relativeBlock = block.getRelative(face); + if (blocks.contains(relativeBlock)) { + relativeBlock.setType(block.getBlockData().getMaterial()); + modifiedBlocks.add(relativeBlock); } - if (blocks.contains(aBlock.getRelative(BlockFace.EAST))) { - aBlock.getRelative(BlockFace.EAST).setType(randomMaterial); - blocks.remove(aBlock.getRelative(BlockFace.EAST)); + } + modifiedBlocks.add(block); + return modifiedBlocks; + } + + /** + * Generates a (optionally multi-tiered) circular layer + * @param center The center of the layer + * @param radii The radii of the layer(s) + * @param safe Whether the layer should be generated without unsafe materials + */ + private static void generateCircularLayer(Location center, int[] radii, boolean safe) { + for (int i = 0; i < radii.length; i++) { + // First generate the basic shape (in this case a circle), + // then fill that shape with clumps from a randomly selected Material list + generateClumps(generateCylinder(center, radii[i], 1, Material.AIR), + safe ? LayerManager.getRandomLayerSafe() : LayerManager.getRandomLayer()); + if (i < radii.length - 1) { + // Another layer will be generated below the current one + // Set that area to AIR on the current level... + generateCylinder(center, radii[i + 1], 1, Material.AIR); + // ...then move down one block to prepare for the next layer + center.setY(center.getY() - 1); } - if (blocks.contains(aBlock.getRelative(BlockFace.WEST))) { - aBlock.getRelative(BlockFace.WEST).setType(randomMaterial); - blocks.remove(aBlock.getRelative(BlockFace.WEST)); + } + } + + /** + * Generates a (optionally multi-tiered) square layer + * @param center The center of the layer + * @param radii The radii of the layer(s) + * @param safe Whether the layer should be generated without unsafe materials + */ + private static void generateSquareLayer(Location center, int[] radii, boolean safe) { + for (int i = 0; i < radii.length; i++) { + // Square generation is similar to circle generation, just with a bit more math + Location pos1 = new Location(center.getWorld(), center.getX() - radii[i], center.getY(), center.getZ() - radii[i]); + Location pos2 = new Location(center.getWorld(), center.getX() + radii[i], center.getY(), center.getZ() + radii[i]); + generateClumps(generateCuboid(pos1, pos2, Material.AIR), + safe ? LayerManager.getRandomLayerSafe() : LayerManager.getRandomLayer()); + if (i < radii.length - 1) { + pos1 = new Location(center.getWorld(), center.getX() - radii[i + 1], center.getY(), center.getZ() - radii[i + 1]); + pos2 = new Location(center.getWorld(), center.getX() + radii[i + 1], center.getY(), center.getZ() + radii[i + 1]); + generateCuboid(pos1, pos2, Material.AIR); + center.setY(center.getY() - 1); } - blocks.remove(aBlock); + } + } + + /** + * Generates multiple circular layer(s), each seperated by `LAYER_DROP_HEIGHT` + * @param center The center of the layer(s) + * @param radii The radii of the layer(s) + * @param layers The amount of layers to generate + */ + private static void generateCircularLayers(Location center, int[] radii, int layers) { + for (int i = 0; i < layers; i++) { + // First layer should always be safe so player can spawn on it + generateCircularLayer(center, radii, i == 0); + center.setY(center.getY() - Generator.LAYER_DROP_HEIGHT); + } + } + + /** + * Generates multiple square layer(s), each seperated by `LAYER_DROP_HEIGHT` + * @param center The center of the layer(s) + * @param radii The radii of the layer(s) + * @param layers The amount of layers to generate + */ + private static void generateSquareLayers(Location center, int[] radii, int layers) { + for (int i = 0; i < layers; i++) { + generateSquareLayer(center, radii, i == 0); + center.setY(center.getY() - Generator.LAYER_DROP_HEIGHT); } } } diff --git a/src/main/java/com/MylesAndMore/Tumble/game/Layers.java b/src/main/java/com/MylesAndMore/Tumble/game/Layers.java deleted file mode 100644 index ed92dc9..0000000 --- a/src/main/java/com/MylesAndMore/Tumble/game/Layers.java +++ /dev/null @@ -1,314 +0,0 @@ -package com.MylesAndMore.Tumble.game; - -import org.bukkit.Material; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -/** - * Stores the different types of layers that can be generated - */ -public class Layers { - - public Layers() { - List gen2 = new ArrayList<>() {{ - add(Material.PINK_TERRACOTTA); - add(Material.PURPLE_TERRACOTTA); - add(Material.GRAY_TERRACOTTA); - add(Material.BLUE_TERRACOTTA); - add(Material.LIGHT_BLUE_TERRACOTTA); - add(Material.WHITE_TERRACOTTA); - add(Material.BROWN_TERRACOTTA); - add(Material.GREEN_TERRACOTTA); - add(Material.YELLOW_TERRACOTTA); - add(Material.PINK_TERRACOTTA); - add(Material.PURPLE_TERRACOTTA); - add(Material.GRAY_TERRACOTTA); - add(Material.BLUE_TERRACOTTA); - add(Material.LIGHT_BLUE_TERRACOTTA); - add(Material.WHITE_TERRACOTTA); - add(Material.BROWN_TERRACOTTA); - add(Material.GREEN_TERRACOTTA); - add(Material.YELLOW_TERRACOTTA); - add(Material.WHITE_STAINED_GLASS); - add(Material.HONEYCOMB_BLOCK); - add(Material.HONEYCOMB_BLOCK); - }}; - List gen4 = new ArrayList<>() {{ - add(Material.DIAMOND_BLOCK); - add(Material.GOLD_BLOCK); - add(Material.REDSTONE_BLOCK); - add(Material.REDSTONE_BLOCK); - add(Material.LAPIS_BLOCK); - add(Material.LAPIS_BLOCK); - add(Material.IRON_BLOCK); - add(Material.COAL_BLOCK); - add(Material.IRON_BLOCK); - add(Material.COAL_BLOCK); - add(Material.IRON_BLOCK); - add(Material.COAL_BLOCK); - add(Material.COAL_BLOCK); - }}; - List gen5 = new ArrayList<>() {{ - add(Material.WHITE_TERRACOTTA); - add(Material.BLUE_ICE); - add(Material.SOUL_SAND); - add(Material.STONE_SLAB); - add(Material.WHITE_TERRACOTTA); - add(Material.BLUE_ICE); - add(Material.SOUL_SAND); - add(Material.STONE_SLAB); - add(Material.WHITE_TERRACOTTA); - add(Material.BLUE_ICE); - add(Material.SOUL_SAND); - add(Material.STONE_SLAB); - add(Material.GLOWSTONE); - add(Material.GLOWSTONE); - add(Material.HONEY_BLOCK); - add(Material.SLIME_BLOCK); - }}; - List gen7 = new ArrayList<>() {{ - add(Material.END_STONE); - add(Material.END_STONE_BRICKS); - add(Material.END_STONE); - add(Material.END_STONE_BRICKS); - add(Material.END_STONE); - add(Material.END_STONE_BRICKS); - add(Material.END_STONE); - add(Material.END_STONE_BRICKS); - add(Material.OBSIDIAN); - add(Material.PURPUR_BLOCK); - add(Material.PURPUR_PILLAR); - add(Material.COBBLESTONE); - }}; - List gen9 = new ArrayList<>() {{ - add(Material.PRISMARINE); - add(Material.DARK_PRISMARINE); - add(Material.BLUE_STAINED_GLASS); - add(Material.WET_SPONGE); - add(Material.PRISMARINE_BRICKS); - add(Material.PRISMARINE_BRICK_SLAB); - add(Material.DARK_PRISMARINE); - add(Material.SEA_LANTERN); - add(Material.TUBE_CORAL_BLOCK); - add(Material.BRAIN_CORAL_BLOCK); - add(Material.BUBBLE_CORAL_BLOCK); - }}; - List gen10 = new ArrayList<>() {{ - add(Material.OAK_LOG); - add(Material.SPRUCE_LOG); - add(Material.ACACIA_LOG); - add(Material.STRIPPED_OAK_LOG); - add(Material.STRIPPED_SPRUCE_LOG); - add(Material.STRIPPED_ACACIA_LOG); - add(Material.OAK_WOOD); - add(Material.SPRUCE_WOOD); - add(Material.ACACIA_WOOD); - add(Material.OAK_LEAVES); - add(Material.SPRUCE_LEAVES); - add(Material.ACACIA_LEAVES); - add(Material.OAK_LEAVES); - add(Material.SPRUCE_LEAVES); - add(Material.ACACIA_LEAVES); - }}; - List gen1 = new ArrayList<>() {{ - add(Material.YELLOW_GLAZED_TERRACOTTA); - add(Material.LIGHT_BLUE_GLAZED_TERRACOTTA); - add(Material.GRAY_GLAZED_TERRACOTTA); - add(Material.PODZOL); - add(Material.PODZOL); - add(Material.PODZOL); - add(Material.ORANGE_GLAZED_TERRACOTTA); - }}; - for (int i = 0; i < 3; i++) { - List gen0 = new ArrayList<>() {{ - add(Material.COAL_ORE); - add(Material.COAL_ORE); - add(Material.COAL_ORE); - add(Material.COAL_ORE); - add(Material.COAL_ORE); - add(Material.IRON_ORE); - add(Material.REDSTONE_ORE); - add(Material.EMERALD_ORE); - add(Material.GOLD_ORE); - add(Material.LAPIS_ORE); - add(Material.DIAMOND_ORE); - add(Material.GRASS_BLOCK); - add(Material.GRASS_BLOCK); - add(Material.GRASS_BLOCK); - add(Material.GRASS_BLOCK); - add(Material.COBWEB); - }}; - matList.add(gen0); - matList.add(gen1); - matList.add(gen2); - List gen3 = new ArrayList<>() {{ - add(Material.PACKED_ICE); - add(Material.PACKED_ICE); - add(Material.NOTE_BLOCK); - add(Material.TNT); - add(Material.LIGHT_BLUE_CONCRETE); - add(Material.GLASS); - add(Material.PACKED_ICE); - add(Material.PACKED_ICE); - add(Material.NOTE_BLOCK); - add(Material.TNT); - add(Material.LIGHT_BLUE_CONCRETE); - add(Material.GLASS); - add(Material.SOUL_SAND); - }}; - matList.add(gen3); - matList.add(gen4); - matList.add(gen5); - List gen6 = new ArrayList<>() {{ - add(Material.NETHERRACK); - add(Material.NETHERRACK); - add(Material.NETHERRACK); - add(Material.NETHER_BRICKS); - add(Material.NETHER_BRICKS); - add(Material.NETHERRACK); - add(Material.NETHERRACK); - add(Material.NETHERRACK); - add(Material.NETHER_BRICKS); - add(Material.NETHER_BRICKS); - add(Material.NETHER_GOLD_ORE); - add(Material.NETHER_GOLD_ORE); - add(Material.CRIMSON_NYLIUM); - add(Material.WARPED_NYLIUM); - add(Material.SOUL_SOIL); - add(Material.CRACKED_NETHER_BRICKS); - add(Material.RED_NETHER_BRICKS); - add(Material.NETHER_WART_BLOCK); - add(Material.CRYING_OBSIDIAN); - add(Material.MAGMA_BLOCK); - }}; - matList.add(gen6); - matList.add(gen7); - List gen8 = new ArrayList<>() {{ - add(Material.REDSTONE_BLOCK); - add(Material.REDSTONE_BLOCK); - add(Material.REDSTONE_LAMP); - add(Material.TARGET); - add(Material.DAYLIGHT_DETECTOR); - add(Material.PISTON); - add(Material.STICKY_PISTON); - add(Material.SLIME_BLOCK); - add(Material.OBSERVER); - add(Material.HOPPER); - }}; - matList.add(gen8); - matList.add(gen9); - matList.add(gen10); - List gen12 = new ArrayList<>() {{ - add(Material.DIRT); - add(Material.DIRT_PATH); - add(Material.GRASS_BLOCK); - add(Material.OAK_SLAB); - add(Material.BRICK_WALL); - add(Material.BRICK_STAIRS); - }}; - matList.add(gen12); - List gen14 = new ArrayList<>() {{ - add(Material.LECTERN); - add(Material.OBSIDIAN); - add(Material.SPONGE); - add(Material.BEEHIVE); - add(Material.DRIED_KELP_BLOCK); - }}; - matList.add(gen14); - List gen15 = new ArrayList<>() {{ - add(Material.SANDSTONE); - add(Material.SANDSTONE_SLAB); - add(Material.RED_SANDSTONE); - add(Material.RED_SANDSTONE_SLAB); - add(Material.RED_TERRACOTTA); - add(Material.TERRACOTTA); - add(Material.YELLOW_TERRACOTTA); - }}; - matList.add(gen15); - List gen16 = new ArrayList<>() {{ - add(Material.JUNGLE_LOG); - add(Material.STRIPPED_JUNGLE_LOG); - add(Material.JUNGLE_WOOD); - add(Material.STRIPPED_JUNGLE_WOOD); - add(Material.MOSSY_COBBLESTONE); - add(Material.MOSSY_COBBLESTONE); - add(Material.MOSSY_COBBLESTONE); - add(Material.JUNGLE_LEAVES); - add(Material.JUNGLE_SLAB); - add(Material.JUNGLE_TRAPDOOR); - }}; - matList.add(gen16); - } - List gen11 = new ArrayList<>() {{ - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.GLASS); - add(Material.WHITE_STAINED_GLASS); - }}; - matList.add(gen11); // Troll glass layer - - for (int i = 0; i < 2; i++) { - safeMatList.add(gen1); - safeMatList.add(gen2); - safeMatList.add(gen4); - safeMatList.add(gen5); - safeMatList.add(gen7); - safeMatList.add(gen9); - safeMatList.add(gen10); - } - safeMatList.add(gen11); // Troll glass layer - } - - // Define Random class - Random random = new Random(); - /** - * @return A random predefined List of Materials that are okay to use in the clump generator - */ - public List getMaterialList() { - return matList.get(random.nextInt(matList.size())); - } - - /** - * @return A random predefined List of Materials that are okay to spawn players on top of - */ - public List getSafeMaterialList() { return safeMatList.get(random.nextInt(safeMatList.size())); } - - // Template: - // private final List gen = new ArrayList<>() {{ - // add(Material. - // }}; - - private final List> matList = new ArrayList<>(); - - private final List> safeMatList = new ArrayList<>(); - -} diff --git a/src/main/java/com/MylesAndMore/Tumble/plugin/Constants.java b/src/main/java/com/MylesAndMore/Tumble/plugin/Constants.java deleted file mode 100644 index 118af23..0000000 --- a/src/main/java/com/MylesAndMore/Tumble/plugin/Constants.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.MylesAndMore.Tumble.plugin; - -import com.onarandombox.MultiverseCore.MultiverseCore; -import com.onarandombox.MultiverseCore.api.MVWorldManager; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; - -import java.util.List; -import java.util.Objects; - -public class Constants { - public static Plugin getPlugin() { - return Bukkit.getServer().getPluginManager().getPlugin("tumble"); - } - public static String getPermissionMessage() { return Constants.getPlugin().getConfig().getString("permissionMessage"); } - public static String getGameWorld() { return Constants.getPlugin().getConfig().getString("gameWorld"); } - public static String getLobbyWorld() { return Constants.getPlugin().getConfig().getString("lobbyWorld"); } - public static String getGameType() { return Constants.getPlugin().getConfig().getString("gameMode"); } - public static List getPlayersInGame() { return Objects.requireNonNull(Bukkit.getServer().getWorld(Constants.getGameWorld())).getPlayers(); } - public static List getPlayersInLobby() { return Objects.requireNonNull(Bukkit.getServer().getWorld(Constants.getLobbyWorld())).getPlayers(); } - - public static MultiverseCore getMV() { return (MultiverseCore) Bukkit.getServer().getPluginManager().getPlugin("Multiverse-Core"); } - public static MVWorldManager getMVWorldManager() { return getMV().getMVWorldManager(); } -} diff --git a/src/main/java/com/MylesAndMore/Tumble/plugin/CustomConfig.java b/src/main/java/com/MylesAndMore/Tumble/plugin/CustomConfig.java new file mode 100644 index 0000000..0ebc82f --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/plugin/CustomConfig.java @@ -0,0 +1,65 @@ +package com.MylesAndMore.Tumble.plugin; + +import com.google.common.base.Charsets; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.logging.Level; + +import static com.MylesAndMore.Tumble.Main.plugin; + +/** + * Allows additional configs to be created with the same saving methods as the default config + * Most code is copied from {@link org.bukkit.plugin.java.JavaPlugin} + */ +public class CustomConfig { + private FileConfiguration newConfig = null; + private final File configFile; + private final String fileName; + + /** + * Create a new CustomConfig + * @param fileName Name of the YAML file to create + */ + public CustomConfig(String fileName) { + this.fileName = fileName; + this.configFile = new File(plugin.getDataFolder(), fileName); + } + + public FileConfiguration getConfig() { + if (newConfig == null) { + reloadConfig(); + } + return newConfig; + } + + public void reloadConfig() { + newConfig = YamlConfiguration.loadConfiguration(configFile); + + final InputStream defConfigStream = plugin.getResource(fileName); + if (defConfigStream == null) { + return; + } + + newConfig.setDefaults(YamlConfiguration.loadConfiguration(new InputStreamReader(defConfigStream, Charsets.UTF_8))); + } + + public void saveConfig() { + try { + getConfig().save(configFile); + } catch (IOException ex) { + plugin.getLogger().log(Level.SEVERE, "Could not save config to " + configFile, ex); + } + } + + public void saveDefaultConfig() { + if (!configFile.exists()) { + plugin.saveResource(fileName, false); + reloadConfig(); + } + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/plugin/EventListener.java b/src/main/java/com/MylesAndMore/Tumble/plugin/EventListener.java deleted file mode 100644 index 9a4dd62..0000000 --- a/src/main/java/com/MylesAndMore/Tumble/plugin/EventListener.java +++ /dev/null @@ -1,222 +0,0 @@ -package com.MylesAndMore.Tumble.plugin; - -import java.util.Objects; - -import com.MylesAndMore.Tumble.game.Game; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.entity.Snowball; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockDropItemEvent; -import org.bukkit.event.entity.*; -import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.event.player.*; -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.Vector; - -/** - * Tumble event listener for all plugin and game-related events. - */ -public class EventListener implements Listener { - @EventHandler - public void PlayerJoinEvent(PlayerJoinEvent event) { - // Hide/show join message accordingly - if (Constants.getPlugin().getConfig().getBoolean("hideJoinLeaveMessages")) { - event.setJoinMessage(null); - } - // Check if either of the worlds are not defined in config, if so, end to avoid any NPEs later on - if (Constants.getGameWorld() == null || Constants.getLobbyWorld() == null) { return; } - if (event.getPlayer().getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - // Send the player back to the lobby if they try to join in the middle of a game - event.getPlayer().teleport(Objects.requireNonNull(Bukkit.getWorld(Constants.getLobbyWorld())).getSpawnLocation()); - } - if (Constants.getPlugin().getConfig().getBoolean("autoStart.enabled")) { - if (Constants.getPlayersInLobby().size() == Constants.getPlugin().getConfig().getInt("autoStart.players")) { - // The autoStart should begin if it is already enabled and the amount of players is correct; pass this to the Game - Game.getGame().autoStart(); - } - } - } - - @EventHandler - public void PlayerChangedWorldEvent(PlayerChangedWorldEvent event) { - if (Constants.getGameWorld() == null || Constants.getLobbyWorld() == null) { - return; - } - if (event.getPlayer().getWorld() == Bukkit.getWorld(Constants.getLobbyWorld())) { - // Another event on which autostart could be triggered - if (Constants.getPlugin().getConfig().getBoolean("autoStart.enabled")) { - if (Constants.getPlayersInLobby().size() == Constants.getPlugin().getConfig().getInt("autoStart.players")) { - Game.getGame().autoStart(); - } - } - } - // Also check if the player left to another world and cancel autostart - else if (event.getFrom() == Bukkit.getWorld(Constants.getLobbyWorld())) { - if (Objects.equals(Game.getGame().getGameState(), "waiting")) { - Game.getGame().cancelStart(); - } - } - } - - @EventHandler - public void PlayerQuitEvent(PlayerQuitEvent event) { - // Hide/show leave message accordingly - if (Constants.getPlugin().getConfig().getBoolean("hideJoinLeaveMessages")) { - event.setQuitMessage(null); - } - if (Constants.getLobbyWorld() == null) { return; } - if (event.getPlayer().getWorld() == Bukkit.getWorld(Constants.getLobbyWorld())) { - // Check if the game is in the process of autostarting, if so cancel - if (Objects.equals(Game.getGame().getGameState(), "waiting")) { - Game.getGame().cancelStart(); - } - } - } - - @EventHandler - public void PlayerDeathEvent(PlayerDeathEvent event) { - if (Constants.getGameWorld() == null) { return; } - // Pass game deaths to the Game - if (event.getEntity().getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - Game.getGame().playerDeath(event.getEntity()); - } - } - - @EventHandler - public void PlayerItemDamageEvent(PlayerItemDamageEvent event) { - if (Constants.getGameWorld() == null) { return; } - // Remove item damage within games - if (event.getPlayer().getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - event.setCancelled(true); - } - } - - @EventHandler - public void ProjectileLaunchEvent(ProjectileLaunchEvent event) { - if (Constants.getGameWorld() == null) { - return; - } - if (event.getEntity().getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - if (event.getEntity() instanceof Snowball) { - if (event.getEntity().getShooter() instanceof Player player) { - // Prevent projectiles (snowballs) from being thrown before the game starts - if (Objects.equals(Game.getGame().getGameState(), "starting")) { - event.setCancelled(true); - } - else { - // Give players a snowball when they've used one (infinite snowballs) - Bukkit.getServer().getScheduler().runTask(Constants.getPlugin(), () -> player.getInventory().addItem(new ItemStack(Material.SNOWBALL, 1))); - } - } - } - } - } - - @EventHandler - public void ProjectileHitEvent(ProjectileHitEvent event) { - if (Constants.getGameWorld() == null) { return; } - else if (event.getHitBlock() == null) { return; } - // Removes blocks that snowballs thrown by players have hit in the game world - if (event.getHitBlock().getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - if (event.getEntity() instanceof Snowball) { - if (event.getEntity().getShooter() instanceof Player) { - if (event.getHitBlock() != null) { - if (event.getHitBlock().getLocation().distanceSquared(Objects.requireNonNull(Bukkit.getWorld(Constants.getGameWorld())).getSpawnLocation()) < 579) { - event.getHitBlock().setType(Material.AIR); - } - } - else if (event.getHitEntity() != null) { - if (event.getHitEntity() instanceof Player hitPlayer) { - // Also cancel any knockback - Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(Constants.getPlugin(), () -> hitPlayer.setVelocity(new Vector())); - } - } - } - } - } - } - - @EventHandler - public void PlayerDropItemEvent(PlayerDropItemEvent event) { - if (Constants.getGameWorld() == null) { return; } - // Don't allow items to drop in the game world - if (event.getPlayer().getWorld() == Bukkit.getWorld((Constants.getGameWorld()))) { - event.setCancelled(true); - } - } - - @EventHandler - public void PlayerMoveEvent(PlayerMoveEvent event) { - if (Constants.getGameWorld() == null) { return; } - // Cancel movement if the game is starting (so players can't move before the game starts) - if (Objects.equals(Game.getGame().getGameState(), "starting")) { - event.setCancelled(true); - } - } - - @EventHandler - public void BlockDropItemEvent(BlockDropItemEvent event) { - if (Constants.getGameWorld() == null) { return; } - // If a block was going to drop an item (ex. snow dropping snowballs) in the game world, cancel it - if (event.getBlock().getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - event.setCancelled(true); - } - } - - @EventHandler - public void PlayerInteractEvent(PlayerInteractEvent event) { - if (Constants.getGameWorld() == null) { return; } - // Remove blocks when clicked in the game world (all gamemodes require this functionality) - if (event.getAction() == Action.LEFT_CLICK_BLOCK) { - if (Objects.requireNonNull(event.getClickedBlock()).getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - event.getClickedBlock().setType(Material.AIR); - } - } - } - - @EventHandler - public void BlockBreakEvent(BlockBreakEvent event) { - if (Constants.getGameWorld() == null) { return; } - // This just doesn't allow blocks to break in the gameWorld; the PlayerInteractEvent will take care of everything - // This prevents any weird client-server desync - if (event.getBlock().getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - event.setCancelled(true); - } - } - - @EventHandler - public void FoodLevelChangeEvent(FoodLevelChangeEvent event) { - if (Constants.getGameWorld() == null) { return; } - // INFINITE FOOD (YAY!!!!) - if (event.getEntity().getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - event.setCancelled(true); - } - } - - @EventHandler - public void EntityDamageEvent(EntityDamageEvent event) { - if (Constants.getGameWorld() == null) { return; } - // Check to see if a player got damaged by another entity (player, snowball, etc) in the gameWorld, if so, cancel it - if (event.getEntity().getWorld() == Bukkit.getWorld(Constants.getGameWorld())) { - if (event.getEntity() instanceof Player) { - if (event.getCause() == EntityDamageEvent.DamageCause.ENTITY_ATTACK || event.getCause() == EntityDamageEvent.DamageCause.ENTITY_SWEEP_ATTACK || event.getCause() == EntityDamageEvent.DamageCause.FALL) { - event.setCancelled(true); - } - } - } - } - - @EventHandler - public void InventoryDragEvent(InventoryDragEvent event) { - if (Constants.getGameWorld() == null) { return; } - if (event.getWhoClicked().getWorld() == Bukkit.getWorld((Constants.getGameWorld()))) { - event.setCancelled(true); - } - } - -} diff --git a/src/main/java/com/MylesAndMore/Tumble/plugin/GameState.java b/src/main/java/com/MylesAndMore/Tumble/plugin/GameState.java new file mode 100644 index 0000000..e27c728 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/plugin/GameState.java @@ -0,0 +1,8 @@ +package com.MylesAndMore.Tumble.plugin; + +public enum GameState { + WAITING, + STARTING, + RUNNING, + ENDING, +} diff --git a/src/main/java/com/MylesAndMore/Tumble/plugin/GameType.java b/src/main/java/com/MylesAndMore/Tumble/plugin/GameType.java new file mode 100644 index 0000000..cf01c88 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/plugin/GameType.java @@ -0,0 +1,11 @@ +package com.MylesAndMore.Tumble.plugin; + +public enum GameType { + SHOVELS, + SNOWBALLS, + MIXED; + + public String toString() { + return this.name().toLowerCase(); + } +} diff --git a/src/main/java/com/MylesAndMore/Tumble/plugin/SubCommand.java b/src/main/java/com/MylesAndMore/Tumble/plugin/SubCommand.java new file mode 100644 index 0000000..cc09527 --- /dev/null +++ b/src/main/java/com/MylesAndMore/Tumble/plugin/SubCommand.java @@ -0,0 +1,12 @@ +package com.MylesAndMore.Tumble.plugin; + +import org.bukkit.command.CommandExecutor; + +/** + * Requires that subCommands have a commandName and permission getter. + * This allows the permission and commandName to be checked from the base command. + */ +public interface SubCommand extends CommandExecutor { + String getCommandName(); + String getPermission(); +} diff --git a/src/main/resources/arenas.yml b/src/main/resources/arenas.yml new file mode 100644 index 0000000..6d3e3bb --- /dev/null +++ b/src/main/resources/arenas.yml @@ -0,0 +1,2 @@ +# NOTE: No coordinate can be equal to zero! Use 0.5 instead if needed. +arenas: {} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml deleted file mode 100644 index 68b4e3d..0000000 --- a/src/main/resources/config.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Customize the default game mode; options include: shovels, snowballs, mixed -gameMode: mixed - -# Customize the auto start feature of Tumble; players can be up to 8 -autoStart: - enabled: false - players: 2 - -# Hides player join/leave messages in public chat -hideJoinLeaveMessages: false - -# Customize the message that displays when the player does not have permission to execute a command from this plugin -permissionMessage: You do not have permission to perform this command! - -# Customize the place that the winner is teleported after a game ends -# Keep in mind that these coordinates cannot be zero! The teleport will fail if any of them are; use something like 0.5 instead -winnerTeleport: - x: - y: - z: - -# The plugin will populate these fields automatically -lobbyWorld: -gameWorld: \ No newline at end of file diff --git a/src/main/resources/language.yml b/src/main/resources/language.yml new file mode 100644 index 0000000..1217a02 --- /dev/null +++ b/src/main/resources/language.yml @@ -0,0 +1,45 @@ +# All plugin chat messages will have this message prepended to them +prefix: "&f[&eTumble&f] " + +# Error messages +unknown-command: "&cUnknown command '%command%'" +no-permission: "&cYou do not have permission to perform this command! &7Required permission: '%permission%.'" +missing-subcommand: "&cMissing sub-command! &7Usage: '/tumble ...'" +missing-arena-parameter: "&cMissing arena name!" +invalid-arena: "&cArena '%arena%' does not exist!" +invalid-type: "&cInvalid game type!" +no-game-in-arena: "&cNo game is currently running in this arena!" +player-not-in-game: "&cYou are not in a game!" +game-full: "&cThis game is full!" +not-for-console: "&cThis cannot be run by the console!" +game-in-progress: "&cThis game is still in progress! &7Wait until it finishes or join another game." +another-type-in-arena: "A game of '%type%' is currently taking place in this arena! &7Choose another arena or join it with '/tumble join %arena% %type%'." +already-in-game: "&cYou are already in a game! Leave it to join another one." +arena-not-ready: "&cThis arena is not yet set up!" +arena-not-ready-op: "&cIncomplete arena. &7Set a game spawn with '/tumble setGameSpawn'." +specify-game-type: "&cNo game is currently taking place in this arena! &7Provide the game type to start one." + +# Success messages +create-success: "&aArena created successfully! &eBefore this arena is usable, you must set a game spawn location with '/tumble setgamespawn'." +forcestart-success: "&aStarting game..." +forcestop-success: "&aGame stopped." +join-success: "&aJoined game &d%arena% - %type%" +leave-success: "&aLeft game &d%arena% - %type%" +reload-success: "&aConfig files reloaded. &eCheck console for possible errors." +remove-success: "&aArena removed." +set-success: "&aLocation set." + +# Game messages +showdown: "&4Showdown!" +lobby-in-10: "&9Returning to lobby in ten seconds..." +waiting-for-players: "&aWaiting for players..." +time-till-start: "&aGame will begin in %wait% seconds!" +round-over: "&cRound over!" +round-winner: "&6%winner% has won the round!" +round-draw: "&6Draw!" +game-over: "&6Game over!" +game-winner: "&6%winner% has won the game!" +count-3: "&23" +count-2: "&e2" +count-1: "&41" +count-go: "&aGo!" diff --git a/src/main/resources/layers.yml b/src/main/resources/layers.yml new file mode 100644 index 0000000..1d39527 --- /dev/null +++ b/src/main/resources/layers.yml @@ -0,0 +1,183 @@ +layers: + ores: + weight: 5 + materials: + - COBBLESTONE 5 + - COAL_ORE 3 + - GRASS_BLOCK 2 + - IRON_ORE + - GOLD_ORE + - REDSTONE_ORE + - EMERALD_ORE + - LAPIS_ORE + - DIAMOND_ORE + - COBWEB + ore_blocks: + weight: 5 + materials: + - COAL_BLOCK 4 + - IRON_BLOCK 3 + - GOLD_BLOCK 2 + - REDSTONE_BLOCK 2 + - LAPIS_BLOCK + - DIAMOND_BLOCK + nether: + weight: 5 + materials: + - NETHERRACK 6 + - NETHER_BRICKS 4 + - NETHER_GOLD_ORE 2 + - NETHER_QUARTZ_ORE 2 + - CRIMSON_NYLIUM + - WARPED_NYLIUM + - CRACKED_NETHER_BRICKS + - RED_NETHER_BRICKS + - NETHER_WART_BLOCK + - CRYING_OBSIDIAN + - SHROOMLIGHT + - BLACKSTONE + - BASALT + - SOUL_SAND + end: + weight: 5 + materials: + - END_STONE 4 + - END_STONE_BRICKS 2 + - PURPUR_BLOCK 2 + - PURPUR_PILLAR + - OBSIDIAN + - COBBLESTONE + redstone: + weight: 4 + materials: + - REDSTONE_BLOCK 2 + - REDSTONE_LAMP + - TARGET + - SLIME_BLOCK + - OBSERVER + - DAYLIGHT_DETECTOR + ocean: + weight: 4 + materials: + - PRISMARINE 2 + - DARK_PRISMARINE + - PRISMARINE_BRICKS + - PRISMARINE_BRICK_SLAB + - BLUE_STAINED_GLASS + - SEA_LANTERN + - SPONGE + - TUBE_CORAL_BLOCK + - BRAIN_CORAL_BLOCK + - BUBBLE_CORAL_BLOCK + desert: + weight: 4 + materials: + - SANDSTONE 2 + - RED_SANDSTONE 2 + - CHISELED_SANDSTONE + - SMOOTH_SANDSTONE + - CUT_SANDSTONE + - SANDSTONE_SLAB + - RED_SANDSTONE_SLAB + - RED_TERRACOTTA + - ORANGE_TERRACOTTA + - YELLOW_TERRACOTTA + - TERRACOTTA + forest: + weight: 4 + materials: + - OAK_LEAVES 2 + - SPRUCE_LEAVES 2 + - ACACIA_LEAVES 2 + - OAK_LOG + - SPRUCE_LOG + - ACACIA_LOG + - STRIPPED_OAK_LOG + - STRIPPED_SPRUCE_LOG + - STRIPPED_ACACIA_LOG + - OAK_WOOD + - SPRUCE_WOOD + - ACACIA_WOOD + jungle: + weight: 3 + materials: + - MOSSY_COBBLESTONE 3 + - COBBLESTONE 2 + - JUNGLE_LEAVES 2 + - JUNGLE_LOG 2 + - STRIPPED_JUNGLE_LOG + - JUNGLE_WOOD + - STRIPPED_JUNGLE_WOOD + - JUNGLE_PLANKS + - JUNGLE_SLAB + overworld: + weight: 3 + materials: + - DIRT + - COARSE_DIRT + - GRASS_BLOCK + - DIRT_PATH # On 1.16.x this must be changed to GRASS_PATH + - MYCELIUM + - PODZOL + - OAK_SLAB + - BRICK_WALL + - BRICK_STAIRS + terracotta: + weight: 3 + materials: + - PINK_TERRACOTTA 2 + - PURPLE_TERRACOTTA 2 + - GRAY_TERRACOTTA 2 + - BLUE_TERRACOTTA 2 + - LIGHT_BLUE_TERRACOTTA 2 + - WHITE_TERRACOTTA 2 + - BROWN_TERRACOTTA 2 + - GREEN_TERRACOTTA 2 + - YELLOW_TERRACOTTA 2 + - HONEYCOMB_BLOCK 2 + - WHITE_STAINED_GLASS + glazed_terracotta: + weight: 3 + materials: + - PODZOL 3 + - YELLOW_GLAZED_TERRACOTTA + - LIGHT_BLUE_GLAZED_TERRACOTTA + - GRAY_GLAZED_TERRACOTTA + sticky: + weight: 3 + materials: + - WHITE_TERRACOTTA 3 + - BLUE_ICE 3 + - STONE_SLAB 3 + - SOUL_SOIL 2 + - GLOWSTONE 2 + - SLIME_BLOCK + - HONEY_BLOCK + annoying_movement: + weight: 2 + materials: + - PACKED_ICE 4 + - JUKEBOX 2 + - TNT 2 + - LIGHT_BLUE_CONCRETE 2 + - GLASS 2 + - SMOOTH_STONE_SLAB 2 + - SOUL_SAND + insanity: + weight: 2 + materials: + - OAK_PLANKS + - OBSIDIAN + - SPONGE + - BEEHIVE + - DRIED_KELP_BLOCK + glass: + weight: 1 + materials: + - GLASS 30 + - WHITE_STAINED_GLASS + +# : +# weight: +# materials: +# - diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 23d50ca..dbea900 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,53 +1,53 @@ main: com.MylesAndMore.Tumble.Main name: Tumble -version: 1.0.4 +version: 2.0.0 description: 'A Minecraft: Java Edition plugin recreating the Tumble minigame from Minecraft Legacy Console Edition.' -api-version: 1.19 -load: STARTUP +api-version: 1.16 +load: POSTWORLD author: MylesAndMore website: https://github.com/MylesAndMore/Tumble -depend: - - Multiverse-Core +softdepend: [Multiverse-Core] + commands: - reload: - description: Reloads the plugin's config. - usage: '§cUsage: /tumble:reload' - permission: tumble.reload - link: - description: Links a world on the server as a lobby/game world. - usage: '§cUsage: /tumble:link (lobby|game)' - permission: tumble.link - aliases: [linkworld, link-world] - start: - description: Force starts a Tumble match with an optional game type. - usage: '§cUsage: /tumble:start [gameType]' - permission: tumble.start - winlocation: - description: Links the location to teleport the winning player of a game. - usage: '§cUsage: /tumble:winlocation [x] [y] [z]' - permission: tumble.winlocation - aliases: [win-location, winloc, win-loc] - autostart: - description: Configures the auto start functions of Tumble. - usage: '§cUsage: /tumble:autostart [enable|disable]' - permission: tumble.autostart - aliases: [auto-start] + tumble: + description: Base command for Tumble + usage: "/tumble ..." + aliases: tmbl + permissions: + tumble.join: + description: Allows you to join a Tumble match. + default: true + tumble.leave: + description: Allows you to leave a Tumble match. + default: true + tumble.forcestart: + description: Allows you to forcibly start a Tumble match. + default: op + tumble.forcestop: + description: Allows you to forcibly stop a Tumble match. + default: op tumble.reload: - description: Allows you to reload the plugin's config. + description: Allows you to reload the Tumble configuration. + default: op + tumble.create: + description: Allows you to create Tumble arenas. + default: op + tumble.remove: + description: Allows you to remove Tumble arenas. default: op - tumble.link: - description: Allows you to link a world on the server as a lobby/game world. + tumble.setgamespawn: + description: Allows you to set the game spawn location for Tumble arenas. default: op - tumble.start: - description: Allows you to start a Tumble match. + tumble.setkillylevel: + description: Allows you to set the kill Y-level for Tumble arenas. default: op - tumble.winlocation: - description: Allows you to link a win location. + tumble.setlobby: + description: Allows you to set the lobby location for Tumble arenas. default: op - tumble.autostart: - description: Allows you to set the autostart details of Tumble. + tumble.setwaitarea: + description: Allows you to set the wait area location for Tumble arenas. default: op - tumble.update: - description: Allows you to get a notification if Tumble is out of date. + tumble.setwinnerlobby: + description: Allows you to set the winner lobby location for Tumble arenas. default: op diff --git a/src/main/resources/settings.yml b/src/main/resources/settings.yml new file mode 100644 index 0000000..28cc4e2 --- /dev/null +++ b/src/main/resources/settings.yml @@ -0,0 +1,9 @@ +# Hides player join and leave messages in public chat during games +hide-join-leave-messages: false + +# Hides player death messages in public chat during games +hide-death-messages: false + +# Duration (in seconds) to wait for more players to join a game before starting +# Set to 0 to disable +wait-duration: 15