diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ae63b95 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.kt text eol=lf \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98e4465..0488563 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,4 +26,4 @@ jobs: uses: actions/upload-artifact@v3 with: name: partygames.jar - path: build/libs/partygames-*-dev-all.jar \ No newline at end of file + path: pgame-plugin/build/libs/partygames-*-all.jar \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3302a67..c88ecc4 100644 --- a/.gitignore +++ b/.gitignore @@ -112,13 +112,13 @@ gradle-app.setting **/build/ # Common working directory -run/ +run # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar .kotlin -src/main/resources/speedbuilders.zip +pgame-plugin/src/main/resources/speedbuilders.zip SnifferHuntTreasureMapPlot \ No newline at end of file diff --git a/README.md b/README.md index 2e6d57e..641f2bc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,242 @@ -## Dependencies +## Structure + +The project is divided into two parts: the core API and the plugin for Mester Network. + +### Core API + +The core API is located in `pgame-api`. It is responsible for any game-related logic, such as loading the world, keeping +track of players, handling game events etc. + +By itself it does not contain any minigames, they have to be registered by external plugins. + +#### Dependencies - AdvancedSlimePaper for MC 1.21.4 + +### Plugin + +The plugin is located in `pgame-plugin`. It's the Party Games plugin for Mester Network and contains the specific +minigames for that server and other non-game related logic, including a leveling system. + +#### Dependencies + - WorldEdit 7.3.10 -- ViaVersion 5.2.1 -- ScoreboardLibrary 2.2.2 -- PlaceholderAPI 2.11.6 \ No newline at end of file +- PlaceholderAPI 2.11.6 + +## API Usage + +### Including the API as dependency + +To include the API as a dependency, add the following to your `build.gradle.kts`: + +```kotlin +dependencies { + compileOnly(project(":pgame-api")) +} +``` + +Or if you're working in a different project, compile the API, copy pgame-api/build/libs/pgame-api--all.jar into +your project's libs folder and add it as a dependency. + +```kotlin +dependencies { + compileOnly(files("libs/pgame-api--all.jar")) +} +``` + +Next, add `PartyGamesCore` as a dependency in your `paper-plugin.yml`: + +```yaml +dependencies: + server: + PartyGamesCore: + load: BEFORE + required: true + join-classpath: true +``` + +### Registering minigames + +To register a minigame, create a class that extends ˙Minigame`. + +The base structure of a minigame class is as follows: + +```kotlin +class MyMinigame( + game: Game, +) : Minigame(game, "minigame_name") +``` + +Don't deviate from this structure, it's important for the PartyGamesCore plugin to work properly. The minigame's name +can be anything you want, the convention is one word, using underscores for spaces. It is also case-insensitive so " +minigame_name" and "Minigame_Name" are the same and will result in an error when both are registered. + +Next, you need to override `name`, `description` and `start`. + +`name` is a `Component` and serves as a display name for the minigame. It is shown when the minigame is about to begin. + +`description` is also a `Component` and serves as a description for the minigame. + +`start` is a function that is executed when the minigame begins. You can use it to initialize the minigame, give items +to the players, summon mobs, etc. + +Here is an example of a simple minigame that gives a stone to every player when the minigame begins: + +```kotlin +class MyMinigame( + game: Game, +) : Minigame(game, "my_minigame") { + override fun start() { + super.start() + for (player in game.onlinePlayers) { + player.inventory.addItem(Material.STONE.createItem(1)) + } + } + + override val name = Component.text("My Minigame") + override val description = Component.text("This is a minigame!") +} +``` + +To extend the functionality of the minigame, you can override event functions such as `handlePlayerInteract` +or `handleBlockBreak`. To view a full list of events, see +the [Minigame](pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt) class. + +#### Listening to more events + +If you want to add more events to the minigame, you can extend the `Minigame` class and override the event functions. + +For example, you might want to listen to the `BlockBreakBlockEvent`. To do that, first extend +the [PartyGamesListener](pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt) file with +the new event: + +```kotlin +@EventHandler +fun onBlockBreakBlock(event: BlockBreakBlockEvent) { + // to access the minigame, you can use the getMinigameFromWorld function, which takes a World object and returns a nullable Minigame + // this also means that the event you're listening to MUST have a way to get the world it's happening in + val minigame = getMinigameFromWorld(event.block.world) + minigame?.handleBlockBreakBlock(event) // the question mark is used to only call the function if the minigame is not null +} +``` + +Then, add this new function to the [Minigame](pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt) +class: + +```kotlin +open fun handleBlockBreakBlock(event: BlockBreakBlockEvent) {} +``` + +By marking the function as open, you can override it in your custom minigame class. + +### Registering bundles + +The API makes a distinction between singular minigames and bundles. A bundle is a collection of at least one minigame, +and this is what's actually playable. + +To register a bundle, you first need to register your minigames. + +In your plugin's `onEnable` function, first get the PartyGamesCore instance: + +```kotlin +val core = PartyGamesCore.getInstance() +``` + +Then, register your minigames: + +```kotlin +core.gameRegistry.registerMinigame( + this, // plugin + MyMinigame::class.qualifiedName!!, // className + "my_minigame", // name + listOf( + // worlds + MinigameWorld("my_minigame", org.bukkit.util.Vector(0.5, 63.0, 0.5)), + ), +) +``` + +Let's break this down: + +- `this` is the JavaPlugin instance of your plugin. +- `MyMinigame::class.qualifiedName!!` is the fully qualified name of your minigame class. (something + like `info.mester.network.testminigame.MyMinigame`) +- `"my_minigame"` is the name of your minigame. This HAS to be the same name you used in your minigame class. + +The final parameter is a list of `MinigameWorld` objects. Each `MinigameWorld` object has a `name` and a `startPos` +property. You can think of tese worlds as the "maps" for your minigame. So if you specify more worlds, the API will +randomly pick one of them to load your minigame in, essentially providing multiple map layouts for the same minigame. +The `name` property is the name of the AdvancedSlimePaper world for the map, and the `startPos` is the starting position +of the minigame. This is where players will spawn when the minigame begins. + +By itself, this minigame is not yet playable, as it is not part of a bundle. You can provide a `registerAs` parameter to +this function to register the minigame as a bundle. + +```kotlin +core.gameRegistry.registerMinigame( + this, + MyMinigame::class.qualifiedName!!, + "my_minigame", + listOf( + MinigameWorld("my_minigame", org.bukkit.util.Vector(0.5, 63.0, 0.5)), + ), + "My Minigame", // registerAs +) +``` + +The `registerAs` parameter is the user-friendly display name for the bundle. If you use register a minigame like this, a +bundle with the same name as the minigame will be created automatically. + +Alternatively, you can register a bundle manually: + +```kotlin +core.gameRegistry.registerBundle( + this, // plugin + listOf("my_minigame"), // minigames + "my_bundle", // name + "My Bundle", // displayName +) +``` + +This will create a bundle with the name "my_bundle" and use only the "my_minigame" as its minigames. This is mostly +useful if you want to register a bundle that contains multiple minigames. + +### Starting a game + +To start a game, you need to get the PartyGamesCore instance: + +```kotlin +val core = PartyGamesCore.getInstance() +``` + +Then, you can start a game using the `startGame` function: + +```kotlin +core.gameRegistry.startGame(players, "my_bundle") +``` + +The `players` parameter is a list of `Player` objects, and the `bundleName` parameter is the name of the bundle to start +the game in. + +This immediately starts the game. If the bundle contains multiple minigames, their order will be randomly selected. + +The `test-minigame` project contains examples of how to write minigames in Kotlin and Java. + +### Creating the minigame worlds + +Before a minigame can be started, you need to register the worlds you specified in the `worlds` parameter +of `registerMinigame` with AdvancedSlimePaper. + +Join the ASP server with the core plugin and your custom plugin, then for each world you want to use, run the following +command: + +```text +/swm create file +``` + +This will create a new, empty world with the name you specified. The world file will be saved in +/slime_worlds/.slime + +Now you can enter this world with `/swm goto ` and start building it to your liking. The world is +periodically auto-saved, but you can always manually save with `/swm save `. To go back to the main world, +use `/swm goto world`. diff --git a/build.gradle.kts b/build.gradle.kts index 946cd13..e675c00 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,92 +1,20 @@ plugins { - kotlin("jvm") version "2.1.0" - id("com.github.johnrengelman.shadow") version "8.1.1" + kotlin("jvm") version "2.1.0" apply false + id("io.papermc.paperweight.userdev") version "2.0.0-beta.14" apply false id("org.sonarqube") version "4.2.1.3168" - id("io.papermc.paperweight.userdev") version "2.0.0-beta.13" - java } -group = "info.mester.network.partygames" -version = "a1.0" +allprojects { + group = "info.mester.network.partygames" + version = "1.0-SNAPSHOT" -repositories { - mavenCentral() - maven("https://repo.papermc.io/repository/maven-public/") - maven("https://oss.sonatype.org/content/groups/public/") - maven("https://maven.enginehub.org/repo/") - maven("https://repo.rapture.pw/repository/maven-releases/") - maven("https://repo.infernalsuite.com/repository/maven-snapshots/") - maven("https://repo.viaversion.com") - maven("https://haoshoku.xyz:8081/repository/default") - maven("https://repo.extendedclip.com/content/repositories/placeholderapi/") -} - -dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - implementation(kotlin("reflect")) - // set up paper - paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") - - compileOnly("com.squareup.okhttp3:okhttp:4.12.0") - compileOnly("net.objecthunter:exp4j:0.4.8") - // WorldEdit - compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.10-SNAPSHOT") - // AdvancedSlimePaper - compileOnly("com.infernalsuite.aswm:api:3.0.0-SNAPSHOT") - // ViaVersion - compileOnly("com.viaversion:viaversion:5.2.1") - // Testing - testImplementation(kotlin("test")) - // ScoreboardLibrary - val scoreboardLibraryVersion = "2.2.2" - implementation("net.megavex:scoreboard-library-api:$scoreboardLibraryVersion") - runtimeOnly("net.megavex:scoreboard-library-implementation:$scoreboardLibraryVersion") - implementation("net.megavex:scoreboard-library-extra-kotlin:$scoreboardLibraryVersion") // Kotlin specific extensions (optional) - runtimeOnly("net.megavex:scoreboard-library-modern:$scoreboardLibraryVersion:mojmap") - // PlaceholderAPI - compileOnly("me.clip:placeholderapi:2.11.6") - // ConfigLib - implementation("de.exlll:configlib-paper:4.5.0") -} -val targetJavaVersion = 21 -kotlin { - jvmToolchain(targetJavaVersion) -} - -tasks { - processResources { - val props = mapOf("version" to version) - inputs.properties(props) - filteringCharset = "UTF-8" - filesMatching("paper-plugin.yml") { - expand(props) - } - } - - build { - dependsOn("shadowJar") - } - - register("writeVersion") { - doLast { - val versionFile = - layout.buildDirectory - .file("version.txt") - .get() - .asFile - versionFile.writeText(project.version.toString()) - } - } - - test { - useJUnitPlatform() + repositories { + mavenCentral() } } -tasks.register("copyPluginToRun") { - dependsOn("build") - from(buildDir.resolve("libs").resolve("partygames-${project.version}-all.jar")) - into(rootDir.resolve("run").resolve("plugins")) +subprojects { + apply(plugin = "org.jetbrains.kotlin.jvm") } sonar { @@ -95,16 +23,3 @@ sonar { property("sonar.projectName", "Bedless Tournament") } } - -sourceSets { - main { - java { - srcDir("src/main/kotlin") - } - } - test { - java { - srcDir("src/test/kotlin") - } - } -} diff --git a/gradle.properties b/gradle.properties index e69de29..66c172f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true \ No newline at end of file diff --git a/pgame-api/build.gradle.kts b/pgame-api/build.gradle.kts new file mode 100644 index 0000000..365ee26 --- /dev/null +++ b/pgame-api/build.gradle.kts @@ -0,0 +1,78 @@ +plugins { + id("com.github.johnrengelman.shadow") version "8.1.1" + id("io.papermc.paperweight.userdev") + id("org.jetbrains.dokka") version "2.0.0" + java +} + +group = "info.mester.network.partygames" +version = "1.0" + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://oss.sonatype.org/content/groups/public/") + maven("https://maven.enginehub.org/repo/") + maven("https://repo.rapture.pw/repository/maven-releases/") + maven("https://repo.infernalsuite.com/repository/maven-snapshots/") +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(kotlin("reflect")) + + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") + compileOnly("com.infernalsuite.aswm:api:3.0.0-SNAPSHOT") +} +val targetJavaVersion = 21 +kotlin { + jvmToolchain(targetJavaVersion) +} + +tasks { + processResources { + val props = mapOf("version" to version) + inputs.properties(props) + filteringCharset = "UTF-8" + filesMatching("paper-plugin.yml") { + expand(props) + } + } + + build { + dependsOn("shadowJar") + dependsOn("dokkaGenerateModuleHtml") + } + + test { + useJUnitPlatform() + } +} + +tasks.register("copyPluginToRun") { + dependsOn("build") + val jarFile = + layout.buildDirectory + .file("libs/pgame-api-${project.version}-all.jar") + .get() + .asFile + val destination = + layout.buildDirectory + .dir("../../run/plugins") + .get() + .asFile + from(jarFile) + into(destination) +} + +sourceSets { + main { + java { + srcDir("src/main/kotlin") + } + } + test { + java { + srcDir("src/test/kotlin") + } + } +} diff --git a/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt similarity index 57% rename from src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt rename to pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt index 9dcb495..144569b 100644 --- a/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt @@ -1,222 +1,195 @@ -package info.mester.network.partygames - -import com.mojang.brigadier.Command -import com.mojang.brigadier.arguments.StringArgumentType -import info.mester.network.partygames.admin.GamesUI -import info.mester.network.partygames.admin.InvseeUI -import info.mester.network.partygames.game.GameType -import info.mester.network.partygames.game.HealthShopMinigame -import info.mester.network.partygames.game.SnifferHuntMinigame -import info.mester.network.partygames.game.SpeedBuildersMinigame -import io.papermc.paper.command.brigadier.Commands -import io.papermc.paper.command.brigadier.argument.ArgumentTypes -import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver -import io.papermc.paper.plugin.bootstrap.BootstrapContext -import io.papermc.paper.plugin.bootstrap.PluginBootstrap -import io.papermc.paper.plugin.bootstrap.PluginProviderContext -import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager -import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.minimessage.MiniMessage -import org.bukkit.entity.Player -import org.bukkit.plugin.java.JavaPlugin -import java.util.UUID - -@Suppress("UnstableApiUsage", "unused") -class Bootstrapper : PluginBootstrap { - val gameLeaveAttempts = mutableMapOf() - - override fun bootstrap(context: BootstrapContext) { - val manager: LifecycleEventManager = context.lifecycleManager - manager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> - val commands = event.registrar() - // /admin - commands.register( - // toggle admin mode - Commands - .literal("admin") - .requires { - it.sender.hasPermission("partygames.admin") - }.executes { ctx -> - val sender = ctx.source.sender - if (sender !is Player) { - return@executes 1 - } - val plugin = PartyGames.plugin - val admin = plugin.isAdmin(sender) - plugin.setAdmin(sender, !admin) - - sender.sendMessage( - MiniMessage - .miniMessage() - .deserialize( - "Admin mode has been ${if (admin) "disabled" else "enabled"}!", - ), - ) - Command.SINGLE_SUCCESS - }.then( - // games - Commands - .literal("games") - .executes { ctx -> - val sender = ctx.source.sender - if (sender !is Player) { - return@executes 1 - } - val ui = GamesUI() - sender.openInventory(ui.getInventory()) - Command.SINGLE_SUCCESS - }, - ).then( - // reload - Commands - .literal("reload") - .executes { ctx -> - val sender = ctx.source.sender - HealthShopMinigame.reload() - SpeedBuildersMinigame.reload() - SnifferHuntMinigame.reload() - PartyGames.plugin.reloadConfig() - PartyGames.plugin.reload() - sender.sendMessage(Component.text("Reloaded the configuration!", NamedTextColor.GREEN)) - Command.SINGLE_SUCCESS - }, - ).then( - // end - Commands - .literal("end") - .executes { ctx -> - val sender = ctx.source.sender - if (sender !is Player) { - return@executes 1 - } - val plugin = PartyGames.plugin - val game = plugin.gameManager.getGameByWorld(sender.world) - if (game == null) { - sender.sendMessage(Component.text("You are not in a game!", NamedTextColor.RED)) - return@executes 1 - } - game.terminate() - Command.SINGLE_SUCCESS - }, - ).build(), - "Main function for managing tournaments", - ) - // /invsee - commands.register( - Commands - .literal("invsee") - .requires { - it.sender.isOp - }.then( - Commands.argument("player", ArgumentTypes.player()).executes { ctx -> - val player = - ctx - .getArgument("player", PlayerSelectorArgumentResolver::class.java) - .resolve(ctx.source)[0] - val ui = InvseeUI(player) - val sender = ctx.source.sender as Player - sender.openInventory(ui.getInventory()) - Command.SINGLE_SUCCESS - }, - ).build(), - "Opens an inventory for the given player", - ) - // /join - commands.register( - Commands - .literal("join") - .then( - Commands - .argument("game", StringArgumentType.word()) - .suggests { ctx, builder -> - kotlin - .runCatching { - val type = StringArgumentType.getString(ctx, "game").uppercase() - for (game in GameType.entries.filter { it.name.uppercase().startsWith(type) }) { - builder.suggest(game.name.lowercase()) - } - }.onFailure { - for (game in GameType.entries) { - builder.suggest(game.name.lowercase()) - } - } - builder.buildFuture() - }.executes { ctx -> - val sender = ctx.source.sender - if (sender !is Player) { - sender.sendMessage( - MiniMessage - .miniMessage() - .deserialize("You have to be a player to run this command!"), - ) - return@executes 1 - } - val typeRaw = StringArgumentType.getString(ctx, "game").uppercase() - if (!GameType.entries.any { it.name.uppercase() == typeRaw }) { - return@executes 1 - } - val type = GameType.valueOf(typeRaw) - val currentQueue = PartyGames.plugin.gameManager.getQueueOf(sender) - if (currentQueue != null && currentQueue.type == type) { - sender.sendMessage( - Component.text( - "You are already in a queue for this game!", - NamedTextColor.RED, - ), - ) - return@executes 1 - } - PartyGames.plugin.gameManager.joinQueue(type, listOf(sender)) - Command.SINGLE_SUCCESS - }, - ).build(), - ) - // leave - commands.register( - Commands - .literal("leave") - .executes { ctx -> - val sender = ctx.source.sender - if (sender !is Player) { - sender.sendMessage( - MiniMessage - .miniMessage() - .deserialize("You have to be a player to run this command!"), - ) - return@executes 1 - } - val gameManager = PartyGames.plugin.gameManager - if (gameManager.getQueueOf(sender) != null) { - gameManager.removePlayerFromQueue(sender) - return@executes Command.SINGLE_SUCCESS - } - if (gameManager.getGameOf(sender) != null) { - val lastLeaveAttempt = gameLeaveAttempts[sender.uniqueId] - if (lastLeaveAttempt != null && System.currentTimeMillis() - lastLeaveAttempt < 5000) { - // leave the game - gameManager.getGameOf(sender)!!.removePlayer(sender) - return@executes Command.SINGLE_SUCCESS - } - gameLeaveAttempts[sender.uniqueId] = System.currentTimeMillis() - sender.sendMessage( - MiniMessage - .miniMessage() - .deserialize( - "You are attempting to leave the game! Run /leave again within 5 seconds to confirm.", - ), - ) - return@executes Command.SINGLE_SUCCESS - } - sender.sendMessage( - MiniMessage.miniMessage().deserialize("You are not in a game or a queue!"), - ) - Command.SINGLE_SUCCESS - }.build(), - ) - } - } - - override fun createPlugin(context: PluginProviderContext): JavaPlugin = PartyGames.plugin -} +package info.mester.network.partygames.api + +import com.mojang.brigadier.Command +import com.mojang.brigadier.arguments.StringArgumentType +import info.mester.network.partygames.api.admin.GamesUI +import info.mester.network.partygames.api.admin.InvseeUI +import io.papermc.paper.command.brigadier.Commands +import io.papermc.paper.command.brigadier.argument.ArgumentTypes +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver +import io.papermc.paper.plugin.bootstrap.BootstrapContext +import io.papermc.paper.plugin.bootstrap.PluginBootstrap +import io.papermc.paper.plugin.bootstrap.PluginProviderContext +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.plugin.java.JavaPlugin +import java.util.UUID + +@Suppress("UnstableApiUsage", "unused") +class Bootstrapper : PluginBootstrap { + val gameLeaveAttempts = mutableMapOf() + + override fun bootstrap(context: BootstrapContext) { + val manager: LifecycleEventManager = context.lifecycleManager + manager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> + val commands = event.registrar() + // /admin + commands.register( + // toggle admin mode + Commands + .literal("admin") + .requires { + it.sender.hasPermission("partygames.admin") + }.executes { ctx -> + val sender = ctx.source.sender + if (sender !is Player) { + return@executes 1 + } + val core = PartyGamesCore.getInstance() + val admin = core.isAdmin(sender) + core.setAdmin(sender, !admin) + + sender.sendMessage( + MiniMessage + .miniMessage() + .deserialize( + "Admin mode has been ${if (admin) "disabled" else "enabled"}!", + ), + ) + Command.SINGLE_SUCCESS + }.then( + // games + Commands + .literal("games") + .executes { ctx -> + val sender = ctx.source.sender + if (sender !is Player) { + return@executes 1 + } + val ui = GamesUI() + sender.openInventory(ui.getInventory()) + Command.SINGLE_SUCCESS + }, + ).then( + // end + Commands + .literal("end") + .executes { ctx -> + val sender = ctx.source.sender + if (sender !is Player) { + return@executes 1 + } + val core = PartyGamesCore.getInstance() + val game = core.gameRegistry.getGameByWorld(sender.world) + if (game == null) { + sender.sendMessage(Component.text("You are not in a game!", NamedTextColor.RED)) + return@executes 1 + } + game.terminate() + Command.SINGLE_SUCCESS + }, + ).then( + // start + Commands + .literal("start") + .then( + Commands + .argument("bundle", StringArgumentType.word()) + .suggests { ctx, builder -> + val core = PartyGamesCore.getInstance() + val bundles = core.gameRegistry.getBundles() + runCatching { + val bundleName = StringArgumentType.getString(ctx.child, "bundle") + bundles + .filter { it.name.uppercase().startsWith(bundleName.uppercase()) } + .map { it.name } + .forEach(builder::suggest) + }.onFailure { + bundles.map { it.name.uppercase() }.forEach(builder::suggest) + } + builder.buildFuture() + }.executes { ctx -> + val core = PartyGamesCore.getInstance() + val bundleName = StringArgumentType.getString(ctx, "bundle") + val players = Bukkit.getOnlinePlayers().toList() + core.gameRegistry.startGame(players, bundleName) + Command.SINGLE_SUCCESS + }, + ), + ).then( + // skip + Commands.literal("skip").executes { ctx -> + val sender = ctx.source.sender + if (sender !is Player) { + return@executes 1 + } + val core = PartyGamesCore.getInstance() + val game = core.gameRegistry.getGameByWorld(sender.world) + if (game == null) { + sender.sendMessage(Component.text("You are not in a game!", NamedTextColor.RED)) + return@executes 1 + } + game.runningMinigame?.end() + Command.SINGLE_SUCCESS + }, + ).build(), + "Main function for managing tournaments", + ) + // /invsee + commands.register( + Commands + .literal("invsee") + .requires { + it.sender.isOp + }.then( + Commands.argument("player", ArgumentTypes.player()).executes { ctx -> + val player = + ctx + .getArgument("player", PlayerSelectorArgumentResolver::class.java) + .resolve(ctx.source)[0] + val ui = InvseeUI(player) + val sender = ctx.source.sender as Player + sender.openInventory(ui.getInventory()) + Command.SINGLE_SUCCESS + }, + ).build(), + "Opens an inventory for the given player", + ) + // leave + commands.register( + Commands + .literal("leave") + .executes { ctx -> + val sender = ctx.source.sender + if (sender !is Player) { + sender.sendMessage( + MiniMessage + .miniMessage() + .deserialize("You have to be a player to run this command!"), + ) + return@executes 1 + } + val gameRegistry = PartyGamesCore.getInstance().gameRegistry + if (gameRegistry.getGameOf(sender) != null) { + val lastLeaveAttempt = gameLeaveAttempts[sender.uniqueId] + if (lastLeaveAttempt != null && System.currentTimeMillis() - lastLeaveAttempt < 5000) { + // leave the game + gameRegistry.getGameOf(sender)!!.removePlayer(sender) + return@executes Command.SINGLE_SUCCESS + } + gameLeaveAttempts[sender.uniqueId] = System.currentTimeMillis() + sender.sendMessage( + MiniMessage + .miniMessage() + .deserialize( + "You are attempting to leave the game! Run /leave again within 5 seconds to confirm.", + ), + ) + return@executes Command.SINGLE_SUCCESS + } + sender.sendMessage( + MiniMessage.miniMessage().deserialize("You are not in a game or a queue!"), + ) + Command.SINGLE_SUCCESS + }.build(), + ) + } + } + + override fun createPlugin(context: PluginProviderContext): JavaPlugin = PartyGamesCore() +} diff --git a/src/main/kotlin/info/mester/network/partygames/game/Game.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt similarity index 71% rename from src/main/kotlin/info/mester/network/partygames/game/Game.kt rename to pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt index ca6d8d6..beb0d16 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/Game.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt @@ -1,10 +1,12 @@ -package info.mester.network.partygames.game +package info.mester.network.partygames.api import com.infernalsuite.aswm.api.AdvancedSlimePaperAPI import com.infernalsuite.aswm.api.world.SlimeWorld -import info.mester.network.partygames.PartyGames -import info.mester.network.partygames.mm -import info.mester.network.partygames.shorten +import info.mester.network.partygames.api.events.GameEndedEvent +import info.mester.network.partygames.api.events.GameStartedEvent +import info.mester.network.partygames.api.events.GameTerminatedEvent +import info.mester.network.partygames.api.events.PlayerRejoinedEvent +import info.mester.network.partygames.api.events.PlayerRemovedFromGameEvent import net.kyori.adventure.audience.Audience import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.key.Key @@ -15,13 +17,39 @@ import net.kyori.adventure.text.minimessage.MiniMessage import org.bukkit.Bukkit import org.bukkit.GameMode import org.bukkit.OfflinePlayer +import org.bukkit.Statistic import org.bukkit.entity.Player import java.util.UUID import java.util.logging.Level -import kotlin.math.floor + +private val mm = MiniMessage.miniMessage() data class PlayerData( - var score: Int, + /** + * Current score of the player. They are used to award stars at the end of the minigame. + * + * Top player = 3 stars + * Second player = 2 stars + * Third player = 1 star + * + * Scores are only stored for a single minigame, and are reset after each minigame. + */ + var score: Int = 0, + /** + * Stars of the player. They are used to determine the winner of the game. + * + * Stars are accumulated over the course of the game, and are not reset after each minigame. + */ + var stars: Int = 0, + /** + * Total score of the player. Not used for anything, custom plugins may use it for their own purposes. + */ + var totalScore: Int = 0, +) + +data class TopPlayerData( + val player: OfflinePlayer, + val data: PlayerData, ) enum class GameState { @@ -36,7 +64,7 @@ enum class GameState { PLAYING, /** - * A minigame has just ended, but the tournament is still running, ready to load the next minigame + * A minigame has just ended, but the game is still running, ready to load the next minigame */ POST_GAME, @@ -52,8 +80,8 @@ enum class GameState { } class Game( - private val plugin: PartyGames, - val type: GameType, + private val core: PartyGamesCore, + val bundle: MinigameBundle, players: List, ) { companion object { @@ -85,7 +113,6 @@ class Game( } private val slimeAPI = AdvancedSlimePaperAPI.instance() - private val sidebarManager = plugin.sidebarManager /** * The unique ID of the game @@ -125,6 +152,11 @@ class Game( private var _state = GameState.STARTING val state get() = _state + /** + * The tick when the game started + */ + val startTime = Bukkit.getCurrentTick() + /** * The boss bar to be used for the remaining time */ @@ -155,16 +187,27 @@ class Game( fun playerData(player: Player) = playerData(player.uniqueId) /** - * Get the top n players in the game + * Get the top n players in the game. + * + * The top players are determined by their stars. The total score is used as a tiebreaker. + * A secret secondary tiebreaker is their total played time statistic. The person who has played less time wins. + * * @param n the number of players to get * @return a list of pairs of the player and their data */ - fun topPlayers(n: Int): List> = + fun topPlayers(n: Int): List = playerDatas .toList() - .sortedByDescending { it.second.score } - .take(n) - .map { Bukkit.getOfflinePlayer(it.first) to it.second } + .sortedWith( + compareByDescending> { it.second.stars } + .thenByDescending { it.second.totalScore } + .thenBy { + Bukkit.getOfflinePlayer(it.first).getStatistic( + Statistic.PLAY_ONE_MINUTE, + ) + }, + ).take(n) + .map { TopPlayerData(Bukkit.getOfflinePlayer(it.first), it.second) } fun topPlayers() = topPlayers(playerDatas.size) @@ -177,70 +220,70 @@ class Game( if (playerDatas.containsKey(player.uniqueId)) { return } - playerDatas[player.uniqueId] = PlayerData(0) + playerDatas[player.uniqueId] = PlayerData() } /** - * Function to remove a player from the game - * @param player the player to remove + * Fully removes a player from the game, calls [Minigame.handleDisconnect]. + * + * @param player the player to remove. */ fun removePlayer(player: Player) { playerDatas.remove(player.uniqueId) handleDisconnect(player, true) if (player.isOnline) { resetPlayer(player) - sidebarManager.openLobbySidebar(player) - player.teleport(plugin.spawnLocation) } + val event = PlayerRemovedFromGameEvent(this, player) + event.callEvent() if (playerDatas.isEmpty()) { end() } } /** - * Get all currently online players in the game + * Gets all currently online players in the game. */ val onlinePlayers get() = playerDatas.keys.toList().mapNotNull { Bukkit.getPlayer(it) } + /** + * Gets all players that are part of the game, including offline players. + */ + val players get() = playerDatas.keys.toList().map { Bukkit.getOfflinePlayer(it) } + fun hasPlayer(player: Player) = playerDatas.contains(player.uniqueId) init { // add all players to the game players.forEach { addPlayer(it) } // set up the game - audience.sendMessage(Component.text("Starting the game...", NamedTextColor.GREEN)) readyMinigames = - type.minigames + bundle.minigames .shuffled() - .map { it.constructors.first().call(this) } - .toTypedArray() - // update the playing placeholder - plugin.playingPlaceholder.addPlaying(type.name, players.size) + .map { + core.gameRegistry + .getMinigame(it)!! + .minigame.constructors + .first() + .call(this) + }.toTypedArray() try { val success = nextMinigame() if (!success) { throw IllegalStateException("Couldn't load the first minigame!") } - // wait a tick and set up the sidebar - Bukkit.getScheduler().runTaskLater( - plugin, - Runnable { - for (player in players) { - plugin.sidebarManager.openGameSidebar(player) - } - }, - 1, - ) + val event = GameStartedEvent(this, players) + event.callEvent() } catch (err: IllegalStateException) { // uh-oh! - plugin.logger.log(Level.SEVERE, "An error occurred while setting up the game!", err) + core.logger.log(Level.SEVERE, "An error occurred while setting up the game!", err) audience.sendMessage(Component.text("An error occurred while setting up the game!", NamedTextColor.RED)) terminate() } } /** - * Begin the async process of loading the next minigame + * Begin the async process of loading the next minigame. */ private fun loadNextMinigame() { if (readyMinigames.isEmpty()) { @@ -257,13 +300,13 @@ class Game( minigameIndex++ _runningMinigame = readyMinigames[minigameIndex] // start an async task to load the world - Bukkit.getAsyncScheduler().runNow(plugin) { + Bukkit.getAsyncScheduler().runNow(core) { // clone the minigame's world into the game's world - val minigameWorld = slimeAPI.getLoadedWorld(_runningMinigame!!.rootWorldName) + val minigameWorld = slimeAPI.getLoadedWorld(_runningMinigame!!.rootWorld.name) val gameWorld = minigameWorld.clone(worldName) // now switch to sync mode Bukkit.getScheduler().runTask( - plugin, + core, Runnable { startIntroduction(gameWorld) }, @@ -275,6 +318,7 @@ class Game( // load the new world slimeAPI.loadWorld(gameWorld, true) val minigame = _runningMinigame as Minigame + minigame.onLoad() audience.sendMessage( Component .text("Welcome to ", NamedTextColor.GREEN) @@ -291,14 +335,14 @@ class Game( if (minigameIndex > 0) { minigameIndex-- // teleport all admins to the new world too - for (admin in world.players.filter { plugin.isAdmin(it) }) { + for (admin in world.players.filter { core.isAdmin(it) }) { admin.teleport(minigame.startPos) } unloadWorld(false) minigameIndex++ } // start a timer that rotates the players around the start pos - Bukkit.getScheduler().runTaskTimer(plugin, IntroductionTimer(this), 0, 1) + Bukkit.getScheduler().runTaskTimer(core, IntroductionTimer(this), 0, 1) } fun hasNextMinigame(): Boolean = minigameIndex < readyMinigames.size - 1 @@ -327,9 +371,36 @@ class Game( player.gameMode = GameMode.SPECTATOR player.teleport(runningMinigame!!.startPos) } + + val topScores = playerDatas.toList().sortedByDescending { it.second.score }.take(3) + // award stars based on the top scores + repeat(3) { i -> + if (i < topScores.size) { + val (uuid, data) = topScores[i] + val player = Bukkit.getOfflinePlayer(uuid) + val newStars = + when (i) { + 0 -> 3 + 1 -> 2 + 2 -> 1 + else -> 0 + } + data.stars += newStars + audience.sendMessage( + mm.deserialize( + "${player.name} has been awarded $newStars★!", + ), + ) + } + } + playerDatas.forEach { (_, data) -> + data.totalScore += data.score + data.score = 0 + } + // wait for 5 seconds and load the new minigame Bukkit.getScheduler().runTaskLater( - plugin, + core, Runnable { val success = nextMinigame() if (!success) { @@ -366,37 +437,35 @@ class Game( */ fun terminate() { _state = GameState.STOPPED - plugin.playingPlaceholder.removePlaying(type.name, playerDatas.size) // this could be the case if we forcefully end the tournament with the command runningMinigame?.terminate() _runningMinigame = null + val event = GameTerminatedEvent(this, playerDatas.size) + event.callEvent() // send everyone to the lobby world and unload the world world.players.forEach { - it.teleport(plugin.spawnLocation) + it.teleport(event.getSpawnLocation()) } unloadWorld() // final cleanup for (player in onlinePlayers) { resetPlayer(player) - plugin.sidebarManager.openLobbySidebar(player) - plugin.showPlayerLevel(player) } - plugin.gameManager.removeGame(this) + core.gameRegistry.removeGame(this) } /** * End the game and announce the winners */ private fun end() { - audience.sendMessage(Component.text("The game has ended!", NamedTextColor.GREEN)) // create a sorted list of player data based on their score val topList = topPlayers() // display the top 3 players val messageLength = 30 val topListMessage = buildString { - append("${"-".repeat(messageLength)}\n") - append("Top players:\n") + appendLine("${"-".repeat(messageLength)}") + appendLine("Top players:") for (i in topList.indices) { val topPlayer = topList.getOrNull(i) @@ -411,65 +480,26 @@ class Game( } else { "" } - append( - "${color}${i + 1}. ${topPlayer?.first?.name ?: "Nobody"} - ${topPlayer?.second?.score ?: 0}\n", + appendLine( + "${color}${i + 1}. ${topPlayer?.player?.name ?: "Nobody"} - ${topPlayer?.data?.stars ?: 0}★", ) } append("${"-".repeat(messageLength)}") } audience.sendMessage(mm.deserialize(topListMessage)) - // increase everyone's xp based on the score - for ((player, data) in topList) { - val oldLevel = plugin.levelManager.levelDataOf(player.uniqueId) - plugin.levelManager.addXp(player.uniqueId, data.score.coerceAtLeast(0)) - val newLevel = plugin.levelManager.levelDataOf(player.uniqueId) - val levelUpMessage = - buildString { - val levelString = "Level: ${oldLevel.level}" - append(levelString) - val leveledUp = newLevel.level > oldLevel.level - if (leveledUp) { - append(" -> ${newLevel.level} LEVEL UP!\n") - } else { - append("\n") - } - append("Progress: ") - append("${newLevel.xp} [") - val maxSquares = 15 - // render the progress bar (we have progressLength squares available) - val progress = (newLevel.xp / newLevel.xpToNextLevel.toFloat()) - val previousProgress = (oldLevel.xp / oldLevel.xpToNextLevel.toFloat()) - val filledSquares = floor(progress * maxSquares).toInt() - var previousFilledSquares = if (leveledUp) 0 else floor(previousProgress * maxSquares).toInt() - // if there are no additional squares, that means we've only earned very little progress - // in that case, the last progress square should always be green to indicate that - var additionalSquares = filledSquares - previousFilledSquares - if (additionalSquares == 0) { - previousFilledSquares -= 1 - additionalSquares = 1 - } - for (i in 0 until previousFilledSquares) { - append("■") - } - for (i in 0 until additionalSquares) { - append("■") - } - for (i in 0 until maxSquares - filledSquares) { - append("■") - } - append("] ${newLevel.xpToNextLevel}\n") - append("${"-".repeat(messageLength)}") - } - Bukkit.getPlayer(player.uniqueId)?.sendMessage(mm.deserialize(levelUpMessage)) - } + val event = GameEndedEvent(this, topList) + event.callEvent() // TODO: add a nice place where people can see the winners as player NPCs, then teleport everyone back in about 10 seconds terminate() } fun handleRejoin(player: Player) { resetPlayer(player) - plugin.sidebarManager.openGameSidebar(player) + val event = PlayerRejoinedEvent(this, player) + if (!event.callEvent()) { + return + } player.gameMode = GameMode.SPECTATOR audience.sendMessage( MiniMessage.miniMessage().deserialize("${player.name} has rejoined the game!"), diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/GameRegistry.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/GameRegistry.kt new file mode 100644 index 0000000..ef83709 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/GameRegistry.kt @@ -0,0 +1,113 @@ +package info.mester.network.partygames.api + +import org.bukkit.World +import org.bukkit.entity.Player +import org.bukkit.plugin.java.JavaPlugin +import org.bukkit.util.Vector +import java.util.UUID +import kotlin.reflect.KClass + +data class MinigameWorld( + val name: String, + val startPos: Vector, + val yaw: Float, + val pitch: Float, + val displayName: String? = null, +) { + constructor(name: String, startPos: Vector) : this( + name, + startPos, + 0f, + 0f, + ) + + fun toLocation(world: World) = startPos.toLocation(world, yaw, pitch) +} + +data class RegisteredMinigame( + val plugin: JavaPlugin, + val minigame: KClass, + val name: String, + val worlds: List, +) + +data class MinigameBundle( + val plugin: JavaPlugin, + val minigames: List, + val name: String, + val displayName: String, +) + +class GameRegistry( + private val core: PartyGamesCore, +) { + private val minigames = mutableListOf() + private val bundles = mutableListOf() + private val games = mutableMapOf() + + fun registerMinigame( + plugin: JavaPlugin, + className: String, + name: String, + worlds: List, + registerAs: String? = null, + ) { + if (worlds.isEmpty()) { + throw IllegalArgumentException("Worlds cannot be empty!") + } + val clazz = plugin.javaClass.classLoader.loadClass(className) + if (!Minigame::class.java.isAssignableFrom(clazz)) { + throw IllegalArgumentException("Class $className is not a subclass of Minigame!") + } + val minigameClazz = clazz.asSubclass(Minigame::class.java) + val kClass = minigameClazz.kotlin + val registeredMinigame = RegisteredMinigame(plugin, kClass, name.uppercase(), worlds) + minigames.add(registeredMinigame) + if (registerAs != null) { + bundles.add(MinigameBundle(plugin, listOf(name), name.uppercase(), registerAs)) + } + } + + fun registerBundle( + plugin: JavaPlugin, + minigames: List, + name: String, + displayName: String, + ) { + bundles.add(MinigameBundle(plugin, minigames, name.uppercase(), displayName)) + } + + fun unregisterPlugin(plugin: JavaPlugin) { + minigames.removeIf { it.plugin.name == plugin.name } + bundles.removeIf { it.plugin.name == plugin.name } + } + + fun getMinigame(name: String): RegisteredMinigame? = minigames.firstOrNull { it.name == name.uppercase() } + + fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name.uppercase() } + + fun getBundles(): List = bundles + + fun startGame( + players: List, + bundleName: String, + ) { + val bundle = getBundle(bundleName) ?: throw IllegalArgumentException("Bundle $bundleName not found!") + val game = Game(core, bundle, players) + games[game.id] = game + } + + fun getGameOf(player: Player) = games.values.firstOrNull { it.hasPlayer(player) } + + fun getGameByWorld(world: World) = games.values.firstOrNull { it.worldName == world.name } + + fun shutdown() { + games.values.forEach { it.terminate() } + } + + fun getGames(): Array = games.values.toTypedArray() + + fun removeGame(game: Game) { + games.remove(game.id) + } +} diff --git a/src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt similarity index 57% rename from src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt rename to pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt index 5e7c8ae..48cd09f 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt @@ -1,21 +1,34 @@ -package info.mester.network.partygames.game +package info.mester.network.partygames.api import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.entity.Player import org.bukkit.scheduler.BukkitTask +import org.bukkit.util.Vector import java.util.function.Consumer -import kotlin.math.atan2 import kotlin.math.cos import kotlin.math.sin -import kotlin.math.sqrt private const val INTRODUCTION_TIME = 8 +enum class IntroductionType { + /** + * The introduction will be a circle around the start position. + * Players will be teleported to the circle and rotated around the start position. + */ + CIRCLE, + + /** + * The introduction will be at the start position. + * The players will be locked to the start position and will only be able to look around. + */ + STATIC, +} + class IntroductionTimer( private val game: Game, ) : Consumer { private var rotation = 0.0 - private var lastTime = System.currentTimeMillis() private var remainingTime = INTRODUCTION_TIME * 20 private fun generateProgressBar(): String { @@ -45,19 +58,30 @@ class IntroductionTimer( val actionBar = "[${generateProgressBar()}]" val players = game.onlinePlayers Audience.audience(players).sendActionBar(MiniMessage.miniMessage().deserialize(actionBar)) + remainingTime-- if (remainingTime <= 0) { t.cancel() game.begin() return } - val deltaTime = System.currentTimeMillis() - lastTime - lastTime = System.currentTimeMillis() - // rotate so that a full revolution takes 20 seconds - rotation += deltaTime * 360.0 / 20000.0 + + when (minigame.introductionType) { + IntroductionType.CIRCLE -> circleIntroduction(minigame, players) + IntroductionType.STATIC -> staticIntroduction(minigame, players) + } + } + + private fun circleIntroduction( + minigame: Minigame, + players: List, + ) { + // rotate so that a full revolution takes 20 seconds (400 ticks) + rotation += 360 / 400.0 if (rotation > 360.0) { - rotation = 0.0 + rotation = rotation % 360.0 } + for (player in players) { // we want to "spread" the players out along the circle, which we can do by // manipulating the degree before calculating the hit position @@ -71,23 +95,35 @@ class IntroductionTimer( // to construct the final location for all players, take the x and z coordinates and set y to startPos.y + 15 val finalPos = minigame.startPos.apply { - val finalX = x + hitX - val finalY = y + 15.0 - val finalZ = z + hitZ - // calculate the yaw and pitch from the final coordinates to the startpos - val dx = x - finalX - val dy = y - (finalY + player.eyeHeight) - val dz = z - finalZ - val distanceXZ = sqrt(dx * dx + dz * dz) - yaw = Math.toDegrees(atan2(dz, dx)).toFloat() - 90 - pitch = -Math.toDegrees(atan2(dy, distanceXZ)).toFloat() - - x = finalX - z = finalZ - y = finalY + x += hitX + y += 15.0 + z += hitZ + val direction = minigame.startPos.toVector().subtract(Vector(x, y, z)) + setDirection(direction) } player.teleportAsync(finalPos) } } + + private fun staticIntroduction( + minigame: Minigame, + players: List, + ) { + for (player in players) { + // teleport the player to the start position and lock their rotation + val playerLocation = player.location + if (playerLocation.world != minigame.startPos.world) { + continue + } + if (playerLocation.distance(minigame.startPos) > 0.01) { + player.teleportAsync( + minigame.startPos.apply { + yaw = player.location.yaw + pitch = player.location.pitch + }, + ) + } + } + } } diff --git a/src/main/kotlin/info/mester/network/partygames/game/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt similarity index 64% rename from src/main/kotlin/info/mester/network/partygames/game/Minigame.kt rename to pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index 135a1f9..6fd5e3f 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -1,6 +1,6 @@ -package info.mester.network.partygames.game +package info.mester.network.partygames.api -import info.mester.network.partygames.PartyGames +import io.papermc.paper.event.block.BlockBreakProgressUpdateEvent import io.papermc.paper.event.entity.EntityMoveEvent import io.papermc.paper.event.player.AsyncChatEvent import io.papermc.paper.event.player.PrePlayerAttackEntityEvent @@ -8,23 +8,32 @@ import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import net.kyori.adventure.text.minimessage.MiniMessage import org.bukkit.Bukkit +import org.bukkit.GameRule import org.bukkit.Location import org.bukkit.entity.Player import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.block.BlockPhysicsEvent import org.bukkit.event.block.BlockPlaceEvent +import org.bukkit.event.entity.CreatureSpawnEvent import org.bukkit.event.entity.EntityChangeBlockEvent import org.bukkit.event.entity.EntityCombustEvent import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent import org.bukkit.event.entity.EntityDismountEvent +import org.bukkit.event.entity.EntityRegainHealthEvent +import org.bukkit.event.entity.EntityShootBowEvent import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryCloseEvent import org.bukkit.event.inventory.InventoryOpenEvent import org.bukkit.event.player.PlayerDropItemEvent import org.bukkit.event.player.PlayerInteractAtEntityEvent import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerItemConsumeEvent import org.bukkit.event.player.PlayerMoveEvent import org.bukkit.event.player.PlayerToggleFlightEvent +import org.bukkit.inventory.Inventory +import org.bukkit.plugin.java.JavaPlugin import org.bukkit.scheduler.BukkitTask import java.util.UUID import java.util.function.Consumer @@ -32,11 +41,15 @@ import kotlin.random.Random abstract class Minigame( protected val game: Game, - startPosPath: String, + minigameName: String, + val introductionType: IntroductionType = IntroductionType.CIRCLE, + val allowFallDamage: Boolean = false, ) { + constructor(game: Game, minigameName: String) : this(game, minigameName, IntroductionType.CIRCLE) + private var _running = false private var countdownUUID = UUID.randomUUID() - protected val plugin = PartyGames.plugin + protected val plugin = PartyGamesCore.getInstance() protected val audience get() = Audience.audience(game.onlinePlayers + game.world.players.filter { plugin.isAdmin(it) }) protected val onlinePlayers get() = game.onlinePlayers val running get() = _running @@ -46,19 +59,17 @@ abstract class Minigame( pos.world = Bukkit.getWorld(game.worldName)!! return pos } - val rootWorldName: String + val rootWorld: MinigameWorld val worldIndex: Int + val originalPlugin: JavaPlugin init { - val startPosConfig = plugin.config.getConfigurationSection("locations.minigames.$startPosPath")!! - val worlds = startPosConfig.getStringList("worlds") - // choose a random world from the list - worldIndex = Random.nextInt(0, worlds.size) - rootWorldName = worlds[worldIndex] - val x = startPosConfig.getDouble("x", 0.0) - val y = startPosConfig.getDouble("y", 0.0) - val z = startPosConfig.getDouble("z", 0.0) - startPos = Location(Bukkit.getWorld(rootWorldName)!!, x, y, z) + val core = PartyGamesCore.getInstance() + val minigameConfig = core.gameRegistry.getMinigame(minigameName)!! + originalPlugin = minigameConfig.plugin + worldIndex = Random.nextInt(0, minigameConfig.worlds.size) + rootWorld = minigameConfig.worlds[worldIndex] + startPos = minigameConfig.worlds[worldIndex].toLocation(Bukkit.getWorld(rootWorld.name)!!) } /** @@ -73,18 +84,29 @@ abstract class Minigame( } } + /** + * Executed when the minigame is loaded and we already have a world ready + * + * Can be used to set up the world (unlike in the constructor, where a world is not yet ready) + */ + open fun onLoad() { + startPos.world.setGameRule(GameRule.FALL_DAMAGE, allowFallDamage) + } + /** * A function to finish the minigame (roll back any changes, handle scores, etc.) + * * This will always run, regardless if the minigame was gracefully ended or not */ open fun finish() {} /** - * Function to stop the minigame (score calculation happens in [finish] + * Function to stop the minigame */ private fun end(nextGame: Boolean) { _running = false + stopCountdown() audience.hideBossBar(game.remainingBossBar) finish() @@ -108,31 +130,32 @@ abstract class Minigame( } private fun updateRemainingTime( - startTime: Long, - duration: Long, + startTime: Int, + duration: Int, ): Boolean { val bar = game.remainingBossBar - val remainingTime = startTime + duration - System.currentTimeMillis() + val remainingTimeTick = startTime + duration - Bukkit.getCurrentTick() + val remainingTime = remainingTimeTick * 0.05 if (remainingTime < 0) { audience.hideBossBar(bar) return false } - val time = remainingTime / 1000 - val minutes = time / 60 - val seconds = time % 60 + val timeSeconds = remainingTime.toInt() + val minutes = timeSeconds / 60 + val seconds = timeSeconds % 60 val name = "Time remaining: $minutes:${seconds.toString().padStart(2, '0')}" bar.name(MiniMessage.miniMessage().deserialize(name)) - bar.progress(remainingTime.toFloat() / duration.toFloat()) + bar.progress(remainingTimeTick.toFloat() / duration.toFloat()) return true } fun startCountdown( - duration: Long, + duration: Int, showBar: Boolean, - onEnd: () -> Unit, + onEnd: Runnable, ) { if (showBar) { audience.showBossBar(game.remainingBossBar) @@ -140,7 +163,7 @@ abstract class Minigame( audience.hideBossBar(game.remainingBossBar) } countdownUUID = UUID.randomUUID() - val startTime = System.currentTimeMillis() + val startTime = Bukkit.getCurrentTick() Bukkit.getScheduler().runTaskTimer( plugin, object : Consumer { @@ -153,7 +176,7 @@ abstract class Minigame( } if (!updateRemainingTime(startTime, duration)) { t.cancel() - onEnd() + onEnd.run() } } }, @@ -163,8 +186,8 @@ abstract class Minigame( } fun startCountdown( - duration: Long, - onEnd: () -> Unit, + duration: Int, + onEnd: Runnable, ) { startCountdown(duration, true, onEnd) } @@ -175,49 +198,83 @@ abstract class Minigame( } // functions for handling events + // entity events open fun handleEntityMove(event: EntityMoveEvent) {} - open fun handlePlayerInteract(event: PlayerInteractEvent) {} + open fun handleEntityChangeBlock(event: EntityChangeBlockEvent) {} - open fun handlePlayerMove(event: PlayerMoveEvent) {} + open fun handleEntityCombust(event: EntityCombustEvent) {} - open fun handleBlockPhysics(event: BlockPhysicsEvent) {} + open fun handleEntityDismount(event: EntityDismountEvent) {} - open fun handleEntityCombust(event: EntityCombustEvent) {} + open fun handleEntityDamage(event: EntityDamageEvent) {} - open fun handlePlayerDeath(event: PlayerDeathEvent) {} + open fun handleEntityDamageByEntity(event: EntityDamageByEntityEvent) {} + + open fun handleEntityRegainHealth(event: EntityRegainHealthEvent) {} + + open fun handleEntityShootBow(event: EntityShootBowEvent) {} + + open fun handleCreatureSpawn( + event: CreatureSpawnEvent, + player: Player, + ) { + } + + // block events + open fun handleBlockPhysics(event: BlockPhysicsEvent) {} open fun handleBlockBreak(event: BlockBreakEvent) {} open fun handleBlockPlace(event: BlockPlaceEvent) {} - open fun handlePrePlayerAttack(event: PrePlayerAttackEntityEvent) {} + open fun handleBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) {} - open fun handleInventoryClose(event: InventoryCloseEvent) {} + // player events + open fun handlePlayerMove(event: PlayerMoveEvent) {} - open fun handlePlayerDropItem(event: PlayerDropItemEvent) {} + open fun handlePlayerInteract(event: PlayerInteractEvent) {} - open fun handleEntityChangeBlock(event: EntityChangeBlockEvent) {} + open fun handlePlayerDeath(event: PlayerDeathEvent) {} - open fun handleInventoryOpen(event: InventoryOpenEvent) {} + open fun handlePrePlayerAttack(event: PrePlayerAttackEntityEvent) {} + + open fun handlePlayerDropItem(event: PlayerDropItemEvent) {} open fun handlePlayerToggleFlight(event: PlayerToggleFlightEvent) {} open fun handlePlayerInteractAtEntity(event: PlayerInteractAtEntityEvent) {} - open fun handleEntityDismount(event: EntityDismountEvent) {} + open fun handlePlayerChat(event: AsyncChatEvent) {} + + open fun handlePlayerItemConsume(event: PlayerItemConsumeEvent) {} + + // inventory events + open fun handleInventoryClose(event: InventoryCloseEvent) {} + open fun handleInventoryOpen(event: InventoryOpenEvent) {} + + open fun handleInventoryClick( + event: InventoryClickEvent, + clickedInventory: Inventory, + ) { + } + // game events + + /** + * Triggers when a player disconnects from the game. + * + * @param player The player who disconnected. + * @param didLeave Indicates if the player left the game (true) or temporarily disconnected (false). + */ open fun handleDisconnect( player: Player, didLeave: Boolean, ) { } - open fun handleRejoin(player: Player) {} - - open fun handlePlayerChat(event: AsyncChatEvent) {} - - open fun handleEntityDamageByEntity(event: EntityDamageByEntityEvent) {} + open fun handleRejoin(player: Player) { + } abstract val name: Component abstract val description: Component diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesCore.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesCore.kt new file mode 100644 index 0000000..f138705 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesCore.kt @@ -0,0 +1,115 @@ +package info.mester.network.partygames.api + +import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.entity.Entity +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.java.JavaPlugin +import java.util.UUID + +fun createBasicItem( + material: Material, + name: String, + count: Int = 1, + vararg lore: String, +): ItemStack { + val item = ItemStack.of(material, count) + item.editMeta { meta -> + meta.displayName(MiniMessage.miniMessage().deserialize("$name")) + meta.lore(lore.map { MiniMessage.miniMessage().deserialize("$it") }) + } + return item +} + +fun UUID.shorten() = this.toString().replace("-", "") + +class PartyGamesCore : JavaPlugin() { + companion object { + private var instance: PartyGamesCore? = null + + fun getInstance(): PartyGamesCore { + if (instance == null) { + throw IllegalStateException("PartyGamesCore has not been initialized!") + } + return instance!! + } + } + + lateinit var gameRegistry: GameRegistry + + private fun updateVisibilityOfPlayer( + playerToChange: Player, + visible: Boolean, + ) { + // change the player's visibility for everyone who isn't an admin + for (player in Bukkit + .getOnlinePlayers() + .filter { it.uniqueId != playerToChange.uniqueId && !isAdmin(it) }) { + if (visible) { + player.hidePlayer(this, playerToChange) + } else { + player.showPlayer(this, playerToChange) + } + } + } + + /** + * List of UUIDs of players who are currently in admin mode + * A user is considered an admin if they are in the admins list + */ + private val admins = mutableListOf() + + /** + * Function to set a player's admin status + * + * @param player the player to manage + * @param isAdmin true if the player should be an admin, false otherwise + */ + fun setAdmin( + player: Player, + isAdmin: Boolean, + ) { + if (isAdmin) { + // make sure the player can see the admins + for (admin in admins) { + player.showPlayer( + this, + Bukkit.getPlayer(admin)!!, + ) + } + admins.add(player.uniqueId) + } else { + // make sure the player can't see the admin + for (admin in admins) { + player.hidePlayer( + this, + Bukkit.getPlayer(admin)!!, + ) + } + admins.remove(player.uniqueId) + } + updateVisibilityOfPlayer(player, isAdmin) + } + + private fun isAdmin(uuid: UUID): Boolean = admins.contains(uuid) + + /** + * Function to check if an entity (usually a player) is an admin + * + * @param entity the entity to check + * @return true if the entity is an admin, false otherwise + */ + fun isAdmin(entity: Entity): Boolean = isAdmin(entity.uniqueId) + + override fun onEnable() { + instance = this + gameRegistry = GameRegistry(this) + Bukkit.getPluginManager().registerEvents(PartyGamesListener(this), this) + } + + override fun onDisable() { + gameRegistry.shutdown() + } +} diff --git a/src/main/kotlin/info/mester/network/partygames/PartyListener.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt similarity index 67% rename from src/main/kotlin/info/mester/network/partygames/PartyListener.kt rename to pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt index 9f37bd0..f2d7889 100644 --- a/src/main/kotlin/info/mester/network/partygames/PartyListener.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt @@ -1,18 +1,12 @@ -package info.mester.network.partygames +package info.mester.network.partygames.api import com.destroystokyo.paper.event.block.AnvilDamagedEvent -import info.mester.network.partygames.admin.InvseeUI -import info.mester.network.partygames.admin.PlayerAdminUI -import info.mester.network.partygames.game.Game -import info.mester.network.partygames.game.GameState -import info.mester.network.partygames.game.HealthShopMinigame -import info.mester.network.partygames.game.SpeedBuildersMinigame -import info.mester.network.partygames.game.healthshop.HealthShopUI +import info.mester.network.partygames.api.admin.InvseeUI +import info.mester.network.partygames.api.admin.PlayerAdminUI import io.papermc.paper.event.block.BlockBreakProgressUpdateEvent import io.papermc.paper.event.entity.EntityMoveEvent import io.papermc.paper.event.player.AsyncChatEvent import io.papermc.paper.event.player.PrePlayerAttackEntityEvent -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import org.bukkit.Bukkit import org.bukkit.World import org.bukkit.attribute.Attribute @@ -20,12 +14,14 @@ import org.bukkit.entity.AbstractArrow import org.bukkit.entity.Arrow import org.bukkit.entity.EntityType import org.bukkit.entity.Player +import org.bukkit.event.Event import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.block.BlockPhysicsEvent import org.bukkit.event.block.BlockPlaceEvent import org.bukkit.event.entity.ArrowBodyCountChangeEvent +import org.bukkit.event.entity.CreatureSpawnEvent import org.bukkit.event.entity.EntityChangeBlockEvent import org.bukkit.event.entity.EntityCombustEvent import org.bukkit.event.entity.EntityDamageByEntityEvent @@ -44,19 +40,22 @@ import org.bukkit.event.player.PlayerInteractAtEntityEvent import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.event.player.PlayerItemConsumeEvent import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerKickEvent import org.bukkit.event.player.PlayerMoveEvent import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.event.player.PlayerSwapHandItemsEvent import org.bukkit.event.player.PlayerToggleFlightEvent -import org.bukkit.event.world.WorldLoadEvent import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.meta.SpawnEggMeta +import java.util.UUID -class PartyListener( - private val plugin: PartyGames, +class PartyGamesListener( + private val core: PartyGamesCore, ) : Listener { - private val gameManager = plugin.gameManager + private val gameRegistry = core.gameRegistry + private val spawnEggUseMap: MutableMap = mutableMapOf() - private fun getMinigameFromWorld(world: World) = gameManager.getGameByWorld(world)?.runningMinigame + private fun getMinigameFromWorld(world: World) = gameRegistry.getGameByWorld(world)?.runningMinigame @EventHandler fun onPlayerInteractAtEntity(event: PlayerInteractAtEntityEvent) { @@ -64,8 +63,8 @@ class PartyListener( return } // check if the player is an admin and if they right-clicked a player while in a game - val game = gameManager.getGameByWorld(event.player.world) - if (PartyGames.plugin.isAdmin(event.player) && + val game = gameRegistry.getGameByWorld(event.player.world) + if (core.isAdmin(event.player) && event.rightClicked is Player && game != null ) { @@ -84,7 +83,7 @@ class PartyListener( fun onInventoryClick(event: InventoryClickEvent) { val clickedInventory = event.clickedInventory ?: return val holder = clickedInventory.getHolder(false) - if (holder is Player) { + if (holder is Player && !core.isAdmin(event.whoClicked)) { // don't let players interact with their armor and offhand if (event.slotType == InventoryType.SlotType.ARMOR || event.slot == 40) { event.isCancelled = true @@ -97,24 +96,25 @@ class PartyListener( holder.onInventoryClick(event) } - if (holder is HealthShopUI) { - event.isCancelled = true - holder.onInventoryClick(event) - } - if (holder is InvseeUI) { event.isCancelled = true return } + val minigame = getMinigameFromWorld(event.whoClicked.world) + minigame?.handleInventoryClick(event, clickedInventory) } @EventHandler fun onEntityDamage(event: EntityDamageEvent) { - // cancel fall damage + // cancel fall damage unless the minigame allows it + val minigame = getMinigameFromWorld(event.entity.world) if (event.entity.type == EntityType.PLAYER && event.cause == EntityDamageEvent.DamageCause.FALL) { - event.isCancelled = true - return + if (minigame?.allowFallDamage != true) { + event.isCancelled = true + return + } } + minigame?.handleEntityDamage(event) } @EventHandler @@ -130,44 +130,9 @@ class PartyListener( @EventHandler fun onAsyncChat(event: AsyncChatEvent) { - if (PartyGames.plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } - // rewrite viewers so only players in the same world can see the message - if (!event.player.hasPermission("partygames.globalchat")) { - val viewers = event.viewers() - viewers.clear() - for (player in event.player.world.players) { - viewers.add(player) - } - } - val plainText = PlainTextComponentSerializer.plainText().serialize(event.message()) - val game = gameManager.getGameOf(event.player) ?: return - // special code for saying "fire map" - if (game.state == GameState.PRE_GAME && - game.runningMinigame is HealthShopMinigame && - game.runningMinigame?.worldIndex == 0 && - plainText == "fire map" - ) { - game.awardPhrase(event.player, plainText, 25, "FIRE MAP!!!!") - } - // special code for saying "gg" - if (game.state == GameState.POST_GAME && !game.hasNextMinigame() && plainText.lowercase() == "gg") { - game.awardPhrase(event.player, "gg", 15, "Good Game") - } - // special code for saying "i wanna lose" - if (plainText.lowercase() == "i wanna lose") { - game.awardPhrase(event.player, "minuspoints", -200, "You wanted it") - } - // special code for "givex" and "losex" - if (plainText.lowercase().startsWith("give") && event.player.hasPermission("partygames.admin")) { - val amount = plainText.substringAfter("give").toIntOrNull() ?: return - game.addScore(event.player, amount, "admin command") - } - if (plainText.lowercase().startsWith("lose") && event.player.hasPermission("partygames.admin")) { - val amount = plainText.substringAfter("lose").toIntOrNull() ?: return - game.addScore(event.player, -amount, "admin command") - } val minigame = getMinigameFromWorld(event.player.world) minigame?.handlePlayerChat(event) } @@ -175,7 +140,7 @@ class PartyListener( @EventHandler fun onPrePlayerAttack(event: PrePlayerAttackEntityEvent) { // by default disable the event for every non-admin player - if (plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } event.isCancelled = true @@ -186,7 +151,7 @@ class PartyListener( @EventHandler fun onInventoryClose(event: InventoryCloseEvent) { - if (plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } val minigame = getMinigameFromWorld(event.player.world) @@ -195,7 +160,7 @@ class PartyListener( @EventHandler fun onPlayerMove(event: PlayerMoveEvent) { - if (plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } val minigame = getMinigameFromWorld(event.player.world) @@ -204,32 +169,28 @@ class PartyListener( @EventHandler fun onPlayerQuit(event: PlayerQuitEvent) { - plugin.setAdmin(event.player, false) - gameManager.getQueueOf(event.player)?.removePlayer(event.player) - gameManager.getGameOf(event.player)?.handleDisconnect(event.player, false) - plugin.sidebarManager.unregisterPlayer(event.player) + core.setAdmin(event.player, false) + gameRegistry.getGameOf(event.player)?.handleDisconnect(event.player, false) } @EventHandler fun onPlayerJoin(event: PlayerJoinEvent) { Game.resetPlayer(event.player) - plugin.showPlayerLevel(event.player) - plugin.sidebarManager.openLobbySidebar(event.player) // make sure admins are hidden from new players - for (admin in Bukkit.getOnlinePlayers().filter { plugin.isAdmin(it) }) { - event.player.hidePlayer(plugin, admin) + for (admin in Bukkit.getOnlinePlayers().filter { core.isAdmin(it) }) { + event.player.hidePlayer(core, admin) } // reset some attributes val attribute = event.player.getAttribute(Attribute.MAX_HEALTH)!! attribute.baseValue = attribute.defaultValue event.player.sendHealthUpdate() // check if the player is still in a game - gameManager.getGameOf(event.player)?.handleRejoin(event.player) + gameRegistry.getGameOf(event.player)?.handleRejoin(event.player) } @EventHandler fun onBlockBreak(event: BlockBreakEvent) { - if (plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } val minigame = getMinigameFromWorld(event.player.world) ?: return @@ -239,16 +200,14 @@ class PartyListener( @EventHandler fun onEntityRegainHealth(event: EntityRegainHealthEvent) { - if (plugin.isAdmin(event.entity)) { + if (core.isAdmin(event.entity)) { return } if (event.entityType != EntityType.PLAYER) { return } val runningMinigame = getMinigameFromWorld(event.entity.world) - if (runningMinigame is HealthShopMinigame) { - runningMinigame.handleEntityRegainHealth(event) - } + runningMinigame?.handleEntityRegainHealth(event) } @EventHandler @@ -266,14 +225,12 @@ class PartyListener( @EventHandler fun onPlayerItemConsume(event: PlayerItemConsumeEvent) { val minigame = getMinigameFromWorld(event.player.world) - if (minigame is HealthShopMinigame) { - minigame.handlePlayerItemConsume(event) - } + minigame?.handlePlayerItemConsume(event) } @EventHandler fun onPlayerDropItem(event: PlayerDropItemEvent) { - if (plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } @@ -284,18 +241,16 @@ class PartyListener( @EventHandler fun onBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) { - if (plugin.isAdmin(event.entity)) { + if (core.isAdmin(event.entity)) { return } val minigame = getMinigameFromWorld(event.entity.world) - if (minigame is SpeedBuildersMinigame) { - minigame.handleBlockBreakProgressUpdate(event) - } + minigame?.handleBlockBreakProgressUpdate(event) } @EventHandler fun onBlockPlace(event: BlockPlaceEvent) { - if (plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } val minigame = getMinigameFromWorld(event.player.world) @@ -310,7 +265,7 @@ class PartyListener( @EventHandler fun onEntityShootBow(event: EntityShootBowEvent) { - if (plugin.isAdmin(event.entity)) { + if (core.isAdmin(event.entity)) { return } // don't allow arrows from being picked up @@ -319,18 +274,25 @@ class PartyListener( projectile.pickupStatus = AbstractArrow.PickupStatus.CREATIVE_ONLY } val minigame = getMinigameFromWorld(event.entity.world) - if (minigame is HealthShopMinigame) { - minigame.handleEntityShootBow(event) - } + minigame?.handleEntityShootBow(event) } @EventHandler fun onPlayerInteract(event: PlayerInteractEvent) { - if (PartyGames.plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } - plugin.gameManager.getQueueOf(event.player)?.handlePlayerInteract(event) getMinigameFromWorld(event.player.world)?.handlePlayerInteract(event) + if (event.useItemInHand() == Event.Result.DENY) { + return + } + // look for spawn eggs + val item = event.item ?: return + if (item.itemMeta !is SpawnEggMeta) return + // Ensure the player is using the item in their main hand + if (event.hand != EquipmentSlot.HAND) return + // Track the player using the spawn egg + spawnEggUseMap[event.player.uniqueId] = System.currentTimeMillis() } @EventHandler @@ -351,12 +313,6 @@ class PartyListener( minigame?.handleEntityCombust(event) } - @EventHandler - fun onWorldLoad(event: WorldLoadEvent) { - val world = event.world - PartyGames.initWorld(world) - } - @EventHandler fun onPlayerSwapHandItems(event: PlayerSwapHandItemsEvent) { event.isCancelled = true @@ -390,4 +346,27 @@ class PartyListener( fun onAnvilDamaged(event: AnvilDamagedEvent) { event.isCancelled = true } + + @EventHandler + fun onPlayerKicked(event: PlayerKickEvent) { + val game = core.gameRegistry.getGameOf(event.player) ?: return + // hacky way to fix this weird bug where the player gets kicked during the introduction + if (game.state == GameState.PRE_GAME && event.cause == PlayerKickEvent.Cause.FLYING_PLAYER) { + event.isCancelled = true + } + } + + @EventHandler + fun onCreatureSpawn(event: CreatureSpawnEvent) { + val minigame = getMinigameFromWorld(event.location.world) ?: return + // We only care about spawns caused by spawn eggs + if (event.spawnReason != CreatureSpawnEvent.SpawnReason.SPAWNER_EGG) return + // Find the player responsible for this spawn + val player = + spawnEggUseMap.entries + .firstOrNull { System.currentTimeMillis() - it.value < 1000 } // Within 1 second + ?.key + ?.let { Bukkit.getPlayer(it) } ?: return + minigame.handleCreatureSpawn(event, player) + } } diff --git a/src/main/kotlin/info/mester/network/partygames/admin/GamesUI.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/GamesUI.kt similarity index 74% rename from src/main/kotlin/info/mester/network/partygames/admin/GamesUI.kt rename to pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/GamesUI.kt index 3b6e5d9..2bc3887 100644 --- a/src/main/kotlin/info/mester/network/partygames/admin/GamesUI.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/GamesUI.kt @@ -1,8 +1,8 @@ -package info.mester.network.partygames.admin +package info.mester.network.partygames.api.admin -import info.mester.network.partygames.PartyGames -import info.mester.network.partygames.shorten -import info.mester.network.partygames.util.createBasicItem +import info.mester.network.partygames.api.PartyGamesCore +import info.mester.network.partygames.api.createBasicItem +import info.mester.network.partygames.api.shorten import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor import org.bukkit.Bukkit @@ -11,7 +11,7 @@ import org.bukkit.inventory.InventoryHolder class GamesUI : InventoryHolder { private val inventory = Bukkit.createInventory(this, 3 * 9, Component.text("Games", NamedTextColor.DARK_GRAY)) - private val gameManager = PartyGames.plugin.gameManager + private val gameRegistry = PartyGamesCore.getInstance().gameRegistry private var page = 0 init { @@ -30,23 +30,22 @@ class GamesUI : InventoryHolder { for (i in 0 until inventory.size) { inventory.setItem(i, border) } - val games = gameManager.getGames() + val games = gameRegistry.getGames() for (index in page * 27 until page * 27 + 27) { if (index >= games.size) { break } val game = games[index] val players = game.onlinePlayers - val playersString = players.take(5).map { "- ${it.name}" }.toTypedArray() + val playersString = players.map { "- ${it.name}" }.toTypedArray() val gameItem = createBasicItem( Material.GREEN_CONCRETE, "${game.id.shorten().substring(0..16)}...", 1, - "Type: ${game.type.name}", + "Bundle: ${game.bundle.name}", "Players: ${players.size}", *playersString, - "...", ) inventory.setItem(index % 27, gameItem) } diff --git a/src/main/kotlin/info/mester/network/partygames/admin/InvseeUI.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/InvseeUI.kt similarity index 97% rename from src/main/kotlin/info/mester/network/partygames/admin/InvseeUI.kt rename to pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/InvseeUI.kt index add3dfc..b642b5f 100644 --- a/src/main/kotlin/info/mester/network/partygames/admin/InvseeUI.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/InvseeUI.kt @@ -1,4 +1,4 @@ -package info.mester.network.partygames.admin +package info.mester.network.partygames.api.admin import net.kyori.adventure.text.minimessage.MiniMessage import org.bukkit.Bukkit diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/PlayerAdminUI.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/PlayerAdminUI.kt new file mode 100644 index 0000000..5c5d1d5 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/PlayerAdminUI.kt @@ -0,0 +1,63 @@ +package info.mester.network.partygames.api.admin + +import info.mester.network.partygames.api.Game +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.InventoryHolder +import org.bukkit.inventory.ItemStack + +class PlayerAdminUI( + private val game: Game, + private val managedPlayer: Player, +) : InventoryHolder { + private val inventory = Bukkit.createInventory(this, 9, Component.text("Admin UI")) + + init { + val openVoiceItem = ItemStack.of(Material.NOTE_BLOCK) + openVoiceItem.editMeta { meta -> + meta.displayName(Component.text("Open Voice Chat").decoration(TextDecoration.ITALIC, false)) + meta.lore( + listOf( + Component + .text("Moves the selected player into the Discord stage") + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false), + ), + ) + } + val playerDataItem = ItemStack.of(Material.PAPER) + playerDataItem.editMeta { meta -> + val playerData = game.playerData(managedPlayer) + if (playerData == null) { + meta.displayName(Component.text("No player data").decoration(TextDecoration.ITALIC, false)) + return@editMeta + } + + meta.displayName(Component.text("Player Data").decoration(TextDecoration.ITALIC, false)) + meta.lore( + listOf( + Component + .text("Score: ${playerData.score}") + .color(NamedTextColor.GRAY) + .decoration(TextDecoration.ITALIC, false), + ), + ) + } + inventory.apply { + setItem(0, openVoiceItem) + setItem(1, playerDataItem) + } + } + + override fun getInventory(): Inventory = inventory + + fun onInventoryClick(event: InventoryClickEvent) { + event.whoClicked.sendMessage(Component.text("TODO")) + } +} diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameEndedEvent.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameEndedEvent.kt new file mode 100644 index 0000000..23a477a --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameEndedEvent.kt @@ -0,0 +1,20 @@ +package info.mester.network.partygames.api.events + +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.TopPlayerData +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +class GameEndedEvent( + val game: Game, + val topList: List, +) : Event() { + companion object { + private val HANDLER_LIST = HandlerList() + + @JvmStatic + fun getHandlerList() = HANDLER_LIST + } + + override fun getHandlers() = HANDLER_LIST +} diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameStartedEvent.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameStartedEvent.kt new file mode 100644 index 0000000..5b74da7 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameStartedEvent.kt @@ -0,0 +1,20 @@ +package info.mester.network.partygames.api.events + +import info.mester.network.partygames.api.Game +import org.bukkit.entity.Player +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +class GameStartedEvent( + val game: Game, + val players: List, +) : Event() { + companion object { + private val HANDLER_LIST = HandlerList() + + @JvmStatic + fun getHandlerList(): HandlerList = HANDLER_LIST + } + + override fun getHandlers() = HANDLER_LIST +} diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameTerminatedEvent.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameTerminatedEvent.kt new file mode 100644 index 0000000..100d471 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameTerminatedEvent.kt @@ -0,0 +1,29 @@ +package info.mester.network.partygames.api.events + +import info.mester.network.partygames.api.Game +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +class GameTerminatedEvent( + val game: Game, + val playerCount: Int, +) : Event() { + companion object { + private val HANDLER_LIST = HandlerList() + + @JvmStatic + fun getHandlerList() = HANDLER_LIST + } + + private var spawnLocation = Bukkit.getWorld("world")!!.spawnLocation + + fun getSpawnLocation() = spawnLocation + + fun setSpawnLocation(spawnLocation: Location) { + this.spawnLocation = spawnLocation + } + + override fun getHandlers() = HANDLER_LIST +} diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/PlayerRejoinedEvent.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/PlayerRejoinedEvent.kt new file mode 100644 index 0000000..697ffb4 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/PlayerRejoinedEvent.kt @@ -0,0 +1,30 @@ +package info.mester.network.partygames.api.events + +import info.mester.network.partygames.api.Game +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +class PlayerRejoinedEvent( + val game: Game, + val player: Player, +) : Event(), + Cancellable { + companion object { + private val HANDLER_LIST = HandlerList() + + @JvmStatic + fun getHandlerList() = HANDLER_LIST + } + + private var cancelled = false + + override fun getHandlers() = HANDLER_LIST + + override fun isCancelled() = cancelled + + override fun setCancelled(cancel: Boolean) { + cancelled = cancel + } +} diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/PlayerRemovedFromGameEvent.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/PlayerRemovedFromGameEvent.kt new file mode 100644 index 0000000..541dc17 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/PlayerRemovedFromGameEvent.kt @@ -0,0 +1,20 @@ +package info.mester.network.partygames.api.events + +import info.mester.network.partygames.api.Game +import org.bukkit.entity.Player +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +class PlayerRemovedFromGameEvent( + val game: Game, + val player: Player, +) : Event() { + companion object { + private val HANDLER_LIST = HandlerList() + + @JvmStatic + fun getHandlerList() = HANDLER_LIST + } + + override fun getHandlers() = HANDLER_LIST +} diff --git a/pgame-api/src/main/resources/paper-plugin.yml b/pgame-api/src/main/resources/paper-plugin.yml new file mode 100644 index 0000000..bf7ad90 --- /dev/null +++ b/pgame-api/src/main/resources/paper-plugin.yml @@ -0,0 +1,13 @@ +name: PartyGamesCore +version: '1.0' +main: info.mester.network.partygames.api.PartyGamesCore +api-version: '1.21' +bootstrapper: info.mester.network.partygames.api.Bootstrapper +load: STARTUP +authors: + - Mester +description: Core API for Party Games +website: https://github.com/MesterNetwork/PartyGames +contributors: + - ChatGPT + - Methamphetamine \ No newline at end of file diff --git a/pgame-plugin/build.gradle.kts b/pgame-plugin/build.gradle.kts new file mode 100644 index 0000000..017b8dc --- /dev/null +++ b/pgame-plugin/build.gradle.kts @@ -0,0 +1,123 @@ +import com.diffplug.spotless.LineEnding + +plugins { + id("com.github.johnrengelman.shadow") version "8.1.1" + id("io.papermc.paperweight.userdev") + id("com.diffplug.spotless") version "7.0.2" + java +} + +group = "info.mester.network.partygames" +version = "1.0" + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://oss.sonatype.org/content/groups/public/") + maven("https://maven.enginehub.org/repo/") + maven("https://repo.rapture.pw/repository/maven-releases/") + maven("https://repo.infernalsuite.com/repository/maven-snapshots/") + maven("https://repo.extendedclip.com/releases/") + maven("https://simonsator.de/repo/") +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(kotlin("reflect")) + compileOnly(project(":pgame-api")) + + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") + // FAWE + implementation(platform("com.intellectualsites.bom:bom-newest:1.52")) + compileOnly("com.fastasyncworldedit:FastAsyncWorldEdit-Core") + compileOnly("com.fastasyncworldedit:FastAsyncWorldEdit-Bukkit") { isTransitive = false } + // AdvancedSlimePaper + compileOnly("com.infernalsuite.aswm:api:3.0.0-SNAPSHOT") + // Testing + testImplementation(kotlin("test")) + // ScoreboardLibrary + val scoreboardLibraryVersion = "2.2.2" + implementation("net.megavex:scoreboard-library-api:$scoreboardLibraryVersion") + runtimeOnly("net.megavex:scoreboard-library-implementation:$scoreboardLibraryVersion") + implementation("net.megavex:scoreboard-library-extra-kotlin:$scoreboardLibraryVersion") // Kotlin specific extensions (optional) + runtimeOnly("net.megavex:scoreboard-library-modern:$scoreboardLibraryVersion:mojmap") + // PlaceholderAPI + compileOnly("me.clip:placeholderapi:2.11.6") + // ConfigLib + implementation("de.exlll:configlib-paper:4.5.0") + // Party and Friends + compileOnly("de.simonsator:Party-and-Friends-MySQL-Edition-Spigot-API:1.6.2-RELEASE") + compileOnly("de.simonsator:spigot-party-api-for-party-and-friends:1.0.7-RELEASE") +} +val targetJavaVersion = 21 +kotlin { + jvmToolchain(targetJavaVersion) +} + +spotless { + kotlin { + targetExclude("build/generated/**/*") + targetExclude("build/generated-src/**/*") + toggleOffOn() + ktlint("1.5.0") + lineEndings = LineEnding.GIT_ATTRIBUTES + } +} + +tasks { + processResources { + val props = mapOf("version" to version) + inputs.properties(props) + filteringCharset = "UTF-8" + filesMatching("paper-plugin.yml") { + expand(props) + } + } + + build { + dependsOn("shadowJar") + } + + register("writeVersion") { + doLast { + val versionFile = + layout.buildDirectory + .file("version.txt") + .get() + .asFile + versionFile.writeText(project.version.toString()) + } + } + + test { + useJUnitPlatform() + } +} + +tasks.register("copyPluginToRun") { + dependsOn("build") + val jarFile = + layout.buildDirectory + .file("libs/pgame-plugin-${project.version}-all.jar") + .get() + .asFile + val destination = + layout.buildDirectory + .dir("../../run/plugins") + .get() + .asFile + from(jarFile) + into(destination) +} + +sourceSets { + main { + java { + srcDir("src/main/kotlin") + } + } + test { + java { + srcDir("src/test/kotlin") + } + } +} diff --git a/src/SpeedBuildersWeightGraph.ggb b/pgame-plugin/src/SpeedBuildersWeightGraph.ggb similarity index 100% rename from src/SpeedBuildersWeightGraph.ggb rename to pgame-plugin/src/SpeedBuildersWeightGraph.ggb diff --git a/pgame-plugin/src/config-schema.json b/pgame-plugin/src/config-schema.json new file mode 100644 index 0000000..162a837 --- /dev/null +++ b/pgame-plugin/src/config-schema.json @@ -0,0 +1,128 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "type": "object", + "properties": { + "minigames": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9_]+$": { + "type": "object", + "properties": { + "worlds": { + "type": "array", + "items": { + "type": "object", + "properties": { + "world": { + "type": "string" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + }, + "yaw": { + "type": "number" + }, + "pitch": { + "type": "number" + } + }, + "required": [ + "world", + "x", + "y", + "z" + ], + "additionalProperties": false + } + }, + "class": { + "type": "string" + }, + "display-name": { + "type": "string" + } + }, + "required": [ + "worlds", + "class" + ], + "additionalProperties": false + } + } + }, + "save-interval": { + "type": "integer", + "minimum": 1 + }, + "spawn-location": { + "type": "object", + "properties": { + "world": { + "type": "string" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + }, + "yaw": { + "type": "number" + }, + "pitch": { + "type": "number" + } + }, + "required": [ + "world", + "x", + "y", + "z", + "yaw", + "pitch" + ] + }, + "mineguessr": { + "type": "object", + "properties": { + "world": { + "type": "string" + }, + "max-size": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "world", + "max-size" + ] + }, + "replace-config": { + "type": "boolean" + }, + "family-night": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "minigames", + "save-interval", + "spawn-location", + "mineguessr", + "family-night" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/health-shop-schema.json b/pgame-plugin/src/health-shop-schema.json similarity index 76% rename from src/health-shop-schema.json rename to pgame-plugin/src/health-shop-schema.json index 5061e16..9ab73a8 100644 --- a/src/health-shop-schema.json +++ b/pgame-plugin/src/health-shop-schema.json @@ -34,9 +34,9 @@ "minimum": 0, "description": "Inventory slot number for the item." }, - "category": { + "group": { "type": "string", - "description": "Category of the item (e.g., sword, sharpness, armor).", + "description": "The group this item belongs to. Only one item of a group can be purchased at a time.", "enum": [ "sword", "sharpness", @@ -46,7 +46,30 @@ "arrow", "power", "punch", - "perk" + "perk", + "ender_pearl", + "tnt", + "fireball", + "offhand", + "regen_ii", + "splash_healing_ii", + "splash_healing", + "speed_ii", + "jump_boost", + "turtle_master", + "levitation", + "blindness", + "poison" + ] + }, + "category": { + "type": "string", + "description": "The category of the item, used for pagination.", + "enum": [ + "combat", + "utility", + "potion", + "miscellaneous" ] }, "amount": { @@ -91,6 +114,14 @@ "z": { "type": "number", "description": "The z coordinate of the spawn location." + }, + "yaw": { + "type": "number", + "description": "The yaw rotation of the spawn location." + }, + "pitch": { + "type": "number", + "description": "The pitch rotation of the spawn location." } }, "required": [ diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/BoosterManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/BoosterManager.kt new file mode 100644 index 0000000..46e63ef --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/BoosterManager.kt @@ -0,0 +1,45 @@ +package info.mester.network.partygames + +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import org.bukkit.entity.Player + +data class Booster( + val name: String, + val multiplier: Double, +) + +class BoosterManager { + private fun getRankBooster(player: Player): Booster? { + if (player.hasPermission("group.insane")) { + return Booster("Insane Booster", 1.75) + } + if (player.hasPermission("group.pro")) { + return Booster("Pro Booster", 1.3) + } + if (player.hasPermission("group.advanced")) { + return Booster("Advanced Booster", 1.15) + } + if (player.hasPermission("group.beginner")) { + return Booster("Beginner Booster", 1.05) + } + return null + } + + /** + * Returns a list of every booster applicable to the player. + * This includes: rank booster, personal booster and global booster. + * @param offlinePlayer the player to get the boosters for + * @return a list of boosters, may be empty + */ + fun getBooster(offlinePlayer: OfflinePlayer): List { + val player = Bukkit.getPlayer(offlinePlayer.uniqueId) ?: return emptyList() + val boosters = mutableListOf() + // process rank booster + val rankBooster = getRankBooster(player) + if (rankBooster != null) { + boosters.add(rankBooster) + } + return boosters + } +} diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt new file mode 100644 index 0000000..f1fdcf7 --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt @@ -0,0 +1,169 @@ +package info.mester.network.partygames + +import com.mojang.brigadier.Command +import com.mojang.brigadier.arguments.StringArgumentType +import info.mester.network.partygames.api.PartyGamesCore +import info.mester.network.partygames.game.GravjumpMinigame +import info.mester.network.partygames.game.HealthShopMinigame +import info.mester.network.partygames.game.healthshop.HealthShopUI +import io.papermc.paper.command.brigadier.Commands +import io.papermc.paper.plugin.bootstrap.BootstrapContext +import io.papermc.paper.plugin.bootstrap.PluginBootstrap +import io.papermc.paper.plugin.bootstrap.PluginProviderContext +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.NamespacedKey +import org.bukkit.attribute.Attribute +import org.bukkit.entity.Player +import org.bukkit.persistence.PersistentDataType +import org.bukkit.plugin.java.JavaPlugin +import java.util.UUID + +@Suppress("UnstableApiUsage", "unused") +class Bootstrapper : PluginBootstrap { + companion object { + val TEST_HEALTHSHOP_UI_KEY = NamespacedKey("partygames", "test_healthshop_ui") + } + + val gameLeaveAttempts = mutableMapOf() + + override fun bootstrap(context: BootstrapContext) { + val manager: LifecycleEventManager = context.lifecycleManager + manager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> + val commands = event.registrar() + // /partygames + commands.register( + Commands + .literal("partygames") + .requires { it.sender.hasPermission("partygames.admin") } + .then( + // reload + Commands + .literal("reload") + .executes { ctx -> + val sender = ctx.source.sender + PartyGames.plugin.reload() + sender.sendMessage(Component.text("Reloaded the configuration!", NamedTextColor.GREEN)) + Command.SINGLE_SUCCESS + }, + ).build(), + ) + // /join + commands.register( + Commands + .literal("join") + .then( + Commands + .argument("game", StringArgumentType.word()) + .suggests { ctx, builder -> + val plugin = PartyGames.plugin + val bundles = + PartyGamesCore + .getInstance() + .gameRegistry + .getBundles() + .filter { it.plugin.name == plugin.name } + .map { it.name.lowercase() } + kotlin + .runCatching { + val type = StringArgumentType.getString(ctx.child, "game").lowercase() + for (game in bundles.filter { it.startsWith(type) }) { + builder.suggest(game.lowercase()) + } + }.onFailure { + for (game in bundles) { + builder.suggest(game.lowercase()) + } + } + builder.buildFuture() + }.executes { ctx -> + val sender = ctx.source.sender + if (sender !is Player) { + sender.sendMessage( + MiniMessage + .miniMessage() + .deserialize("You have to be a player to run this command!"), + ) + return@executes 1 + } + val bundleRaw = StringArgumentType.getString(ctx, "game").uppercase() + val bundle = + PartyGamesCore + .getInstance() + .gameRegistry + .getBundle(bundleRaw) + ?: run { + sender.sendMessage( + MiniMessage + .miniMessage() + .deserialize("Game $bundleRaw not found!"), + ) + return@executes 1 + } + + val currentQueue = PartyGames.plugin.queueManager.getQueueOf(sender) + if (currentQueue != null && currentQueue.bundle == bundle) { + sender.sendMessage( + Component.text( + "You are already in a queue for this game!", + NamedTextColor.RED, + ), + ) + return@executes 1 + } + PartyGames.plugin.queueManager.joinQueue(bundle, sender) + Command.SINGLE_SUCCESS + }, + ).build(), + ) + + // gravjump + commands.register( + Commands + .literal("gravjump") + .requires { it.sender.hasPermission("partygames.gravjump") } + .then( + Commands.literal("flip").executes { ctx -> + // get the game + val sender = ctx.source.sender as? Player ?: return@executes 1 + val game = + PartyGamesCore.getInstance().gameRegistry.getGameByWorld(sender.world) + ?: return@executes 1 + val minigame = game.runningMinigame as? GravjumpMinigame ?: return@executes 1 + + minigame.flip() + Command.SINGLE_SUCCESS + }, + ).build(), + ) + + // healthshop + commands.register( + Commands + .literal("healthshop") + .requires { it.sender.hasPermission("partygames.healthshop") } + .then( + Commands.literal("testui").executes { ctx -> + val player = ctx.source.sender as? Player ?: return@executes 1 + val healthShop = HealthShopUI(player.uniqueId, HealthShopMinigame.startingHealth) + player.openInventory(healthShop.inventory) + player.persistentDataContainer.set(TEST_HEALTHSHOP_UI_KEY, PersistentDataType.BOOLEAN, true) + + player.getAttribute(Attribute.MAX_HEALTH)?.apply { + baseValue = HealthShopMinigame.startingHealth + player.health = baseValue + player.sendHealthUpdate() + } + + Command.SINGLE_SUCCESS + }, + ).build(), + ) + } + } + + override fun createPlugin(context: PluginProviderContext): JavaPlugin = PartyGames() +} diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt new file mode 100644 index 0000000..c59d515 --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt @@ -0,0 +1,267 @@ +package info.mester.network.partygames + +import info.mester.network.partygames.game.HealthShopMinigame +import info.mester.network.partygames.game.healthshop.HealthShopKit +import info.mester.network.partygames.level.LevelData +import java.io.File +import java.sql.Connection +import java.sql.DriverManager +import java.util.UUID + +class DatabaseManager( + databaseFile: File, +) { + private val connection: Connection + + init { + // init JDBC driver + Class.forName("org.sqlite.JDBC") + // create database file + if (!databaseFile.exists()) { + databaseFile.createNewFile() + } + // connect to database + connection = DriverManager.getConnection("jdbc:sqlite:${databaseFile.absolutePath}") + // create tables if they don't exist + connection.createStatement().use { statement -> + statement.executeUpdate("CREATE TABLE IF NOT EXISTS levels (uuid CHAR(32) PRIMARY KEY, level INTEGER, exp INTEGER)") + statement.executeUpdate( + """ + CREATE TABLE IF NOT EXISTS games_won ( + uuid CHAR(32), + game TEXT, + amount INTEGER, + PRIMARY KEY (uuid, game) + ); + """.trimIndent(), + ) + statement.executeUpdate( + """ + CREATE TABLE IF NOT EXISTS points_gained ( + uuid CHAR(32), + game TEXT, + amount INTEGER, + PRIMARY KEY (uuid, game) + ); + """.trimIndent(), + ) + statement.executeUpdate( + """ + CREATE TABLE IF NOT EXISTS time_played ( + uuid CHAR(32), + game TEXT, + amount INTEGER, + PRIMARY KEY (uuid, game) + ); + """.trimIndent(), + ) + statement.executeUpdate( + """ + CREATE TABLE IF NOT EXISTS healthshop_kits ( + uuid CHAR(32), + "index" INTEGER, + items TEXT, + PRIMARY KEY (uuid, "index") + ) + """.trimIndent(), + ) + } + } + + fun getLevel(uuid: UUID): LevelData? { + val statement = connection.prepareStatement("SELECT * FROM levels WHERE uuid = ?") + statement.setString(1, uuid.shorten()) + val resultSet = statement.executeQuery() + if (resultSet.next()) { + val level = resultSet.getInt("level") + val exp = resultSet.getInt("exp") + return LevelData(level, exp) + } else { + return null + } + } + + fun saveLevel( + uuid: UUID, + levelData: LevelData, + ) { + val statement = connection.prepareStatement("INSERT OR REPLACE INTO levels (uuid, level, exp) VALUES (?, ?, ?)") + statement.setString(1, uuid.shorten()) + statement.setInt(2, levelData.level) + statement.setInt(3, levelData.xp) + statement.executeUpdate() + } + + fun addGameWon( + uuid: UUID, + game: String?, + ) { + val statement = + connection.prepareStatement( + """ + INSERT INTO games_won (uuid, game, amount) + VALUES (?, ?, 1) + ON CONFLICT(uuid, game) DO UPDATE SET amount = amount + 1; + """.trimIndent(), + ) + statement.setString(1, uuid.shorten()) + statement.setString(2, game?.lowercase() ?: "__global") + statement.executeUpdate() + } + + fun getGamesWon( + uuid: UUID, + game: String?, + ): Int { + val statementString = + if (game == null) { + "SELECT amount FROM games_won WHERE uuid = ?" + } else { + "SELECT amount FROM games_won WHERE uuid = ? AND game = ?" + } + val statement = connection.prepareStatement(statementString) + statement.setString(1, uuid.shorten()) + if (game != null) { + statement.setString(2, game.lowercase()) + } + val resultSet = statement.executeQuery() + var totalGamesWon = 0 + while (resultSet.next()) { + totalGamesWon += resultSet.getInt("amount") + } + + return totalGamesWon + } + + fun addPointsGained( + uuid: UUID, + game: String, + amount: Int, + ) { + val statement = + connection.prepareStatement( + """ + INSERT INTO points_gained (uuid, game, amount) + VALUES (?, ?, ?) + ON CONFLICT(uuid, game) DO UPDATE SET amount = amount + ?; + """.trimIndent(), + ) + statement.setString(1, uuid.shorten()) + statement.setString(2, game.lowercase()) + statement.setInt(3, amount) + statement.setInt(4, amount) + statement.executeUpdate() + } + + fun getPointsGained( + uuid: UUID, + game: String?, + ): Int { + val statementString = + if (game == null) { + "SELECT amount FROM points_gained WHERE uuid = ?" + } else { + "SELECT amount FROM points_gained WHERE uuid = ? AND game = ?" + } + val statement = connection.prepareStatement(statementString) + statement.setString(1, uuid.shorten()) + if (game != null) { + statement.setString(2, game.lowercase()) + } + val resultSet = statement.executeQuery() + var totalPointsGained = 0 + while (resultSet.next()) { + totalPointsGained += resultSet.getInt("amount") + } + + return totalPointsGained + } + + fun addTimePlayed( + uuid: UUID, + game: String, + amount: Int, + ) { + val statement = + connection.prepareStatement( + """ + INSERT INTO time_played (uuid, game, amount) + VALUES (?, ?, ?) + ON CONFLICT(uuid, game) DO UPDATE SET amount = amount + ?; + """.trimIndent(), + ) + statement.setString(1, uuid.shorten()) + statement.setString(2, game.lowercase()) + statement.setInt(3, amount) + statement.setInt(4, amount) + statement.executeUpdate() + } + + fun getTimePlayed( + uuid: UUID, + game: String?, + ): Int { + val statementString = + if (game == null) { + "SELECT amount FROM time_played WHERE uuid = ?" + } else { + "SELECT amount FROM time_played WHERE uuid = ? AND game = ?" + } + val statement = connection.prepareStatement(statementString) + statement.setString(1, uuid.shorten()) + if (game != null) { + statement.setString(2, game.lowercase()) + } + val resultSet = statement.executeQuery() + // if game is null -> add everything together + // else -> just return the amount for that game + var totalTimePlayed = 0 + while (resultSet.next()) { + totalTimePlayed += resultSet.getInt("amount") + } + + return totalTimePlayed + } + + fun saveHealthShopKit( + uuid: UUID, + kit: HealthShopKit, + ) { + val statement = + connection.prepareStatement( + """ + INSERT OR REPLACE INTO healthshop_kits (uuid, items, "index") + VALUES (?, ?, ?) + """.trimIndent(), + ) + statement.setString(1, uuid.shorten()) + statement.setString(2, kit.items.joinToString(",") { it.key }) + statement.setInt(3, kit.index) + statement.executeUpdate() + } + + fun getHealthShopKits(uuid: UUID): List { + val statement = connection.prepareStatement("SELECT * FROM healthshop_kits WHERE uuid = ?") + statement.setString(1, uuid.shorten()) + val resultSet = statement.executeQuery() + val kits = mutableListOf() + while (resultSet.next()) { + val itemsString = resultSet.getString("items") + val index = resultSet.getInt("index") + val healthShopItems = HealthShopMinigame.getShopItems() + val items = itemsString.split(",").mapNotNull { key -> healthShopItems.firstOrNull { it.key == key } } + kits.add(HealthShopKit(items, index)) + } + return kits + } + + fun deleteHealthShopKit( + uuid: UUID, + index: Int, + ) { + val statement = connection.prepareStatement("DELETE FROM healthshop_kits WHERE uuid = ? AND \"index\" = ?") + statement.setString(1, uuid.shorten()) + statement.setInt(2, index) + statement.executeUpdate() + } +} diff --git a/src/main/kotlin/info/mester/network/partygames/PartyGames.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt similarity index 52% rename from src/main/kotlin/info/mester/network/partygames/PartyGames.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt index 08b0830..e887131 100644 --- a/src/main/kotlin/info/mester/network/partygames/PartyGames.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt @@ -1,24 +1,28 @@ package info.mester.network.partygames -import com.viaversion.viaversion.api.Via -import com.viaversion.viaversion.api.ViaAPI -import info.mester.network.partygames.admin.updateVisibilityOfPlayer -import info.mester.network.partygames.game.GameManager +import info.mester.network.partygames.api.MinigameWorld +import info.mester.network.partygames.api.PartyGamesCore +import info.mester.network.partygames.game.GravjumpMinigame +import info.mester.network.partygames.game.HealthShopMinigame +import info.mester.network.partygames.game.MineguessrMinigame +import info.mester.network.partygames.game.QueueManager +import info.mester.network.partygames.game.SnifferHuntMinigame +import info.mester.network.partygames.game.SpeedBuildersMinigame import info.mester.network.partygames.level.LevelManager -import info.mester.network.partygames.level.LevelPlaceholder +import info.mester.network.partygames.placeholder.LevelPlaceholder +import info.mester.network.partygames.placeholder.PlayingPlaceholder +import info.mester.network.partygames.placeholder.StatisticsPlaceholder import info.mester.network.partygames.sidebar.SidebarManager import net.kyori.adventure.text.minimessage.MiniMessage import net.megavex.scoreboardlibrary.api.ScoreboardLibrary import net.megavex.scoreboardlibrary.api.exception.NoPacketAdapterAvailableException import net.megavex.scoreboardlibrary.api.noop.NoopScoreboardLibrary -import okhttp3.OkHttpClient import org.bukkit.Bukkit import org.bukkit.GameRule -import org.bukkit.Location import org.bukkit.World -import org.bukkit.entity.Entity import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin +import org.bukkit.util.Vector import java.io.File import java.util.UUID import kotlin.math.pow @@ -67,18 +71,19 @@ fun Int.toRomanNumeral(): String { fun Int.pow(power: Int): Double = this.toDouble().pow(power) class PartyGames : JavaPlugin() { - lateinit var gameManager: GameManager - lateinit var viaAPI: ViaAPI<*> + lateinit var core: PartyGamesCore + lateinit var queueManager: QueueManager lateinit var scoreboardLibrary: ScoreboardLibrary lateinit var playingPlaceholder: PlayingPlaceholder lateinit var databaseManager: DatabaseManager lateinit var levelManager: LevelManager - lateinit var spawnLocation: Location + val spawnLocation get() = config.getLocation("spawn-location")!! lateinit var sidebarManager: SidebarManager + lateinit var boosterManager: BoosterManager companion object { - val plugin = PartyGames() - val httpClient = OkHttpClient() + private var _plugin: PartyGames? = null + val plugin get() = _plugin!! fun initWorld(world: World) { world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) @@ -94,46 +99,6 @@ class PartyGames : JavaPlugin() { } } - /** - * List of UUIDs of players who are currently in admin mode - * A user is considered an admin if they are in the admins list - */ - private val admins = mutableListOf() - - /** - * Function to set a player's admin status - * - * @param player the player to manage - * @param isAdmin true if the player should be an admin, false otherwise - */ - fun setAdmin( - player: Player, - isAdmin: Boolean, - ) { - if (isAdmin) { - // make sure the player can see the admins - for (admin in admins) { - player.showPlayer( - this, - Bukkit.getPlayer(admin)!!, - ) - } - admins.add(player.uniqueId) - } else { - // make sure the player can't see the admin - for (admin in admins) { - player.hidePlayer( - this, - Bukkit.getPlayer(admin)!!, - ) - } - admins.remove(player.uniqueId) - } - updateVisibilityOfPlayer(player, isAdmin) - } - - private fun isAdmin(uuid: UUID): Boolean = admins.contains(uuid) - fun showPlayerLevel(player: Player) { // if the player is in the lobby world, set their xp to their level val levelData = levelManager.levelDataOf(player.uniqueId) @@ -141,24 +106,26 @@ class PartyGames : JavaPlugin() { player.exp = levelData.xp / levelData.xpToNextLevel.toFloat() } - /** - * Function to check if an entity (usually a player) is an admin - * - * @param entity the entity to check - * @return true if the entity is an admin, false otherwise - */ - fun isAdmin(entity: Entity): Boolean = isAdmin(entity.uniqueId) - fun reload() { - spawnLocation = config.getLocation("spawn-location")!! + reloadConfig() + registerMinigames() + // reload minigame configs + HealthShopMinigame.reload() + SpeedBuildersMinigame.reload() + SnifferHuntMinigame.reload() + MineguessrMinigame.reload() + GravjumpMinigame.reload() } override fun onEnable() { - saveResource("config.yml", true) - saveResource("health-shop.yml", true) - saveResource("speed-builders.yml", true) - saveResource("sniffer-hunt.yml", true) - spawnLocation = config.getLocation("spawn-location")!! + _plugin = this + core = PartyGamesCore.getInstance() + saveResource("config.yml", false) + saveResource("health-shop.yml", false) + saveResource("speed-builders.yml", false) + saveResource("sniffer-hunt.yml", false) + saveResource("gravjump.yml", false) + reload() // register low-level APIs try { scoreboardLibrary = ScoreboardLibrary.loadScoreboardLibrary(this) @@ -166,16 +133,17 @@ class PartyGames : JavaPlugin() { logger.warning("Failed to load ScoreboardLibrary, fallbacking to no-op") scoreboardLibrary = NoopScoreboardLibrary() } - viaAPI = Via.getAPI() // register managers databaseManager = DatabaseManager(File(dataFolder, "partygames.db")) levelManager = LevelManager(this) - gameManager = GameManager(this) + boosterManager = BoosterManager() + queueManager = QueueManager(this) sidebarManager = SidebarManager(this) // register placeholders - playingPlaceholder = PlayingPlaceholder() + playingPlaceholder = PlayingPlaceholder(this) playingPlaceholder.register() LevelPlaceholder(levelManager).register() + StatisticsPlaceholder(databaseManager).register() // set up event listeners server.pluginManager.registerEvents(PartyListener(this), this) // init all worlds @@ -184,9 +152,50 @@ class PartyGames : JavaPlugin() { } } + private fun registerMinigames() { + core.gameRegistry.unregisterPlugin(this) + val minigames = config.getConfigurationSection("minigames")!! + for (minigameName in minigames.getKeys(false)) { + val minigameConfig = minigames.getConfigurationSection(minigameName)!! + val worldsList = minigameConfig.getList("worlds")!! + val worlds = + worldsList.mapNotNull { entry -> + if (entry is Map<*, *>) { + val world = entry["world"] as String + val x = entry["x"] as Double + val y = entry["y"] as Double + val z = entry["z"] as Double + val yaw = entry["yaw"] as? Double ?: 0.0 + val pitch = entry["pitch"] as? Double ?: 0.0 + val displayName = entry["display-name"] as? String + MinigameWorld(world, Vector(x, y, z), yaw.toFloat(), pitch.toFloat(), displayName) + } else { + null + } + } + val className = minigameConfig.getString("class")!! + val displayName = minigameConfig.getString("display-name") + core.gameRegistry.registerMinigame( + this, + className, + minigameName, + worlds, + displayName, + ) + } + // register bundles for each minigame, then family night + core.gameRegistry.registerBundle( + this, + config.getStringList("family-night"), + "familynight", + "Family Night", + ) + } + override fun onDisable() { // Plugin shutdown logic - gameManager.shutdown() - levelManager.stop() + if (::levelManager.isInitialized) { + levelManager.stop() + } } } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt new file mode 100644 index 0000000..ee48375 --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt @@ -0,0 +1,370 @@ +package info.mester.network.partygames + +import info.mester.network.partygames.api.GameState +import info.mester.network.partygames.api.events.GameEndedEvent +import info.mester.network.partygames.api.events.GameStartedEvent +import info.mester.network.partygames.api.events.GameTerminatedEvent +import info.mester.network.partygames.api.events.PlayerRejoinedEvent +import info.mester.network.partygames.api.events.PlayerRemovedFromGameEvent +import info.mester.network.partygames.game.HealthShopMinigame +import info.mester.network.partygames.game.SpeedBuildersMinigame +import info.mester.network.partygames.game.healthshop.HealthShopUI +import info.mester.network.partygames.util.snapTo90 +import io.papermc.paper.event.player.AsyncChatEvent +import io.papermc.paper.event.player.PrePlayerAttackEntityEvent +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import org.bukkit.Bukkit +import org.bukkit.GameMode +import org.bukkit.attribute.Attribute +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.entity.ArrowBodyCountChangeEvent +import org.bukkit.event.entity.CreatureSpawnEvent +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerMoveEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.event.world.WorldLoadEvent +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.meta.SpawnEggMeta +import org.bukkit.persistence.PersistentDataType +import java.util.UUID +import kotlin.math.floor +import kotlin.math.roundToInt + +class PartyListener( + private val plugin: PartyGames, +) : Listener { + private val gameManager = plugin.queueManager + private val core = plugin.core + private val sidebarManager = plugin.sidebarManager + + @EventHandler + fun onAsyncChat(event: AsyncChatEvent) { + if (core.isAdmin(event.player)) { + return + } + // rewrite viewers so only players in the same world can see the message + if (!event.player.hasPermission("partygames.globalchat")) { + val viewers = event.viewers() + viewers.clear() + for (player in event.player.world.players) { + viewers.add(player) + } + } + val plainText = PlainTextComponentSerializer.plainText().serialize(event.message()) + val game = core.gameRegistry.getGameOf(event.player) ?: return + // special code for saying "fire map" + if (game.state == GameState.PRE_GAME && + game.runningMinigame is HealthShopMinigame && + game.runningMinigame?.worldIndex == 0 && + plainText == "fire map" + ) { + game.awardPhrase(event.player, plainText, 25, "FIRE MAP!!!!") + } + // special code for saying "gg" + if (game.state == GameState.POST_GAME && !game.hasNextMinigame() && plainText.lowercase() == "gg") { + game.awardPhrase(event.player, "gg", 15, "Good Game") + } + // remove points for saying "ez" when the game is ending + if (game.state == GameState.POST_GAME && + !game.hasNextMinigame() && + plainText + .split(" ") + .any { it.lowercase() == "ez" } + ) { + game.awardPhrase(event.player, "ez", -30, "Disrespectful behaviour") + } + // special code for saying "i wanna lose" + if (plainText.lowercase() == "i wanna lose") { + game.awardPhrase(event.player, "minuspoints", -200, "You wanted it") + } + // special code for "givex" and "losex" + if (plainText.lowercase().startsWith("give") && event.player.hasPermission("partygames.admin")) { + val amount = plainText.substringAfter("give").toIntOrNull() ?: return + game.addScore(event.player, amount, "admin command") + } + if (plainText.lowercase().startsWith("lose") && event.player.hasPermission("partygames.admin")) { + val amount = plainText.substringAfter("lose").toIntOrNull() ?: return + game.addScore(event.player, -amount, "admin command") + } + } + + @EventHandler + fun onPlayerQuit(event: PlayerQuitEvent) { + gameManager.removePlayerFromQueue(event.player) + plugin.sidebarManager.unregisterPlayer(event.player) + } + + @EventHandler + fun onPlayerJoin(event: PlayerJoinEvent) { + plugin.showPlayerLevel(event.player) + plugin.sidebarManager.openLobbySidebar(event.player) + } + + @EventHandler + fun onArrowBodyCountChange(event: ArrowBodyCountChangeEvent) { + // players should not have arrows stuck in their butts + event.newAmount = 0 + } + + @EventHandler + fun onPlayerInteract(event: PlayerInteractEvent) { + if (plugin.core.isAdmin(event.player)) { + return + } + plugin.queueManager.getQueueOf(event.player)?.handlePlayerInteract(event) + } + + @EventHandler + fun onWorldLoad(event: WorldLoadEvent) { + val world = event.world + PartyGames.initWorld(world) + } + + @EventHandler + fun onGameStarted(event: GameStartedEvent) { + val game = event.game + plugin.playingPlaceholder.addPlaying(game.bundle.name, event.players.size) + Bukkit.getScheduler().runTaskLater( + plugin, + Runnable { + for (player in event.players) { + sidebarManager.openGameSidebar(player) + } + }, + 1, + ) + } + + @EventHandler + fun onGameTerminated(event: GameTerminatedEvent) { + val game = event.game + plugin.playingPlaceholder.removePlaying(game.bundle.name, event.playerCount) + for (player in event.game.onlinePlayers) { + plugin.sidebarManager.openLobbySidebar(player) + plugin.showPlayerLevel(player) + } + event.setSpawnLocation(plugin.spawnLocation) + } + + @EventHandler + fun onGameEnded(event: GameEndedEvent) { + /** + * Time played in seconds. + */ + val timeElapsed = (Bukkit.getCurrentTick() - event.game.startTime) / 20 + + for ((placement, topList) in event.topList.withIndex()) { + val player = topList.player + val data = topList.data + + // 0.8 * total points gained + val xpFromScore = (data.totalScore.coerceAtLeast(0) * 0.8).roundToInt() + // 15 xp every 30 seconds of playtime + val xpFromPlayTime = (timeElapsed / 30) * 15 + // 50 xp for top 1 + // 40 xp for top 2 + // 30 xp for top 3 + // 10 xp for top 5 + val xpFromPlacement = + when (placement) { + 0 -> 50 + 1 -> 40 + 2 -> 30 + in 3..4 -> 10 + else -> 0 + } + // 20 xp for each star + val xpFromStars = data.stars * 20 + val totalXp = xpFromScore + xpFromPlayTime + xpFromPlacement + xpFromStars + + plugin.databaseManager.addPointsGained( + player.uniqueId, + event.game.bundle.name, + data.totalScore.coerceAtLeast(0), + ) + plugin.databaseManager.addTimePlayed(player.uniqueId, event.game.bundle.name, timeElapsed) + + // process boosters + val boosters = plugin.boosterManager.getBooster(player) + val boosterMultiplier = boosters.fold(1.0) { acc, booster -> acc * booster.multiplier } + val finalXp = (totalXp * boosterMultiplier).toInt() + + val oldLevel = plugin.levelManager.levelDataOf(player.uniqueId) + plugin.levelManager.addXp(player.uniqueId, finalXp) + val onlinePlayer = Bukkit.getPlayer(player.uniqueId) ?: return + + // send level up message + val newLevel = plugin.levelManager.levelDataOf(player.uniqueId) + val levelUpMessage = + buildString { + val levelString = "Level: ${oldLevel.level}" + append(levelString) + val leveledUp = newLevel.level > oldLevel.level + if (leveledUp) { + appendLine(" -> ${newLevel.level} LEVEL UP!") + } else { + appendLine() + } + append("Progress: ") + append("${newLevel.xp} [") + val maxSquares = 15 + // render the progress bar (we have progressLength squares available) + val progress = (newLevel.xp / newLevel.xpToNextLevel.toFloat()) + val previousProgress = (oldLevel.xp / oldLevel.xpToNextLevel.toFloat()) + val filledSquares = floor(progress * maxSquares).toInt() + var previousFilledSquares = if (leveledUp) 0 else floor(previousProgress * maxSquares).toInt() + // if there are no additional squares, that means we've only earned very little progress + // in that case, the last progress square should always be green to indicate that + var additionalSquares = filledSquares - previousFilledSquares + if (additionalSquares == 0 && (newLevel.xp - oldLevel.xp) > 0) { + previousFilledSquares -= 1 + additionalSquares = 1 + } + append("" + "■".repeat(previousFilledSquares)) + append("" + "■".repeat(additionalSquares)) + append("" + "■".repeat(maxSquares - filledSquares)) + appendLine("] ${newLevel.xpToNextLevel}") + + appendLine("+$xpFromStars XP (Stars Earned)") + appendLine("+$xpFromScore XP (Points Gained)") + appendLine("+$xpFromPlacement XP (Placement)") + appendLine("+$xpFromPlayTime XP (Time Played)") + for (booster in boosters) { + append( + "+${((booster.multiplier - 1) * 100).toInt()}% (${booster.name})\n", + ) + } + appendLine("= $finalXp XP") + append("${"-".repeat(30)}") + } + onlinePlayer.sendMessage(mm.deserialize(levelUpMessage)) + } + + // increase games won stat + plugin.databaseManager.addGameWon( + event.topList + .first() + .player.uniqueId, + event.game.bundle.name, + ) + } + + @EventHandler + fun onPlayerRejoined(event: PlayerRejoinedEvent) { + val player = event.player + Bukkit.getScheduler().runTaskLater( + plugin, + Runnable { + plugin.sidebarManager.openGameSidebar(player) + }, + 1, + ) + } + + @EventHandler + fun onPlayerRemovedFromGame(event: PlayerRemovedFromGameEvent) { + val player = event.player + if (player.isOnline) { + plugin.sidebarManager.openLobbySidebar(player) + player.teleport(plugin.spawnLocation) + } + plugin.playingPlaceholder.removePlaying(event.game.bundle.name, 1) + } + + @EventHandler + fun onPlayerMove(event: PlayerMoveEvent) { + val player = event.player + if (core.isAdmin(player)) { + return + } + if (player.world.name == "world" && event.to.y < 50 && player.gameMode == GameMode.SURVIVAL) { + player.teleport(plugin.spawnLocation) + } + } + + private val spawnEggUseMap: MutableMap = mutableMapOf() + + @EventHandler + fun playerUsesSpawnEgg(event: PlayerInteractEvent) { + val player = event.player + if (!player.hasPermission("partygames.spawnegg")) { + return + } + if (core.gameRegistry.getGameOf(player) != null) { + return + } + val item = event.item ?: return + // Check if the item is a spawn egg + if (item.itemMeta !is SpawnEggMeta) return + // Ensure the player is using the item in their main hand + if (event.hand != EquipmentSlot.HAND) return + // Track the player using the spawn egg + spawnEggUseMap[player.uniqueId] = System.currentTimeMillis() + } + + @EventHandler + fun onCreatureSpawn(event: CreatureSpawnEvent) { + // We only care about spawns caused by spawn eggs + if (event.spawnReason != CreatureSpawnEvent.SpawnReason.SPAWNER_EGG) return + // Find the player responsible for this spawn + val player = + spawnEggUseMap.entries + .firstOrNull { System.currentTimeMillis() - it.value < 1000 } // Within 1 second + ?.key + ?.let { Bukkit.getPlayer(it) } ?: return + val snappedAngle = snapTo90(player.location.yaw) + val spawnee = event.entity + spawnee.setRotation(snappedAngle, 0.0f) + spawnee.setAI(false) + spawnee.isSilent = true + spawnee.persistentDataContainer.set( + SpeedBuildersMinigame.SPAWNED_ENTITY_KEY, + PersistentDataType.BOOLEAN, + true, + ) + } + + @EventHandler + fun onPrePlayerAttack(event: PrePlayerAttackEntityEvent) { + val player = event.player + if (core.gameRegistry.getGameOf(player) != null) { + return + } + if (!player.hasPermission("partygames.spawnegg")) { + return + } + val target = event.attacked + if (target.persistentDataContainer.has(SpeedBuildersMinigame.SPAWNED_ENTITY_KEY, PersistentDataType.BOOLEAN)) { + target.remove() + } + } + + @EventHandler + fun onInventoryClick(event: InventoryClickEvent) { + val holder = event.clickedInventory?.getHolder(false) + if (holder is HealthShopUI) { + // we handle Health Shop UI clicks here so we can create a test UI + event.isCancelled = true + holder.onInventoryClick(event) + } + } + + @EventHandler + fun onInventoryClose(event: InventoryCloseEvent) { + val holder = event.inventory.holder + val player = event.player as? Player ?: return + if (holder is HealthShopUI && player.persistentDataContainer.has(Bootstrapper.TEST_HEALTHSHOP_UI_KEY)) { + player.getAttribute(Attribute.MAX_HEALTH)?.apply { + baseValue = defaultValue + player.health = baseValue + player.sendHealthUpdate() + } + player.persistentDataContainer.remove(Bootstrapper.TEST_HEALTHSHOP_UI_KEY) + } + } +} diff --git a/src/main/kotlin/info/mester/network/partygames/UUIDDataType.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/UUIDDataType.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/UUIDDataType.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/UUIDDataType.kt diff --git a/src/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt similarity index 91% rename from src/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt index 720e86c..467e9f5 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt @@ -1,5 +1,7 @@ package info.mester.network.partygames.game +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.Minigame import info.mester.network.partygames.mm import info.mester.network.partygames.pow import info.mester.network.partygames.util.WeightedItem @@ -14,6 +16,7 @@ import org.bukkit.GameRule import org.bukkit.Location import org.bukkit.Material import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.LivingEntity import org.bukkit.entity.Monster import org.bukkit.entity.Player import org.bukkit.entity.Spider @@ -135,7 +138,8 @@ data class DamageDealerItem( } } -class DamageDealer( +@Suppress("Unused") +class DamageDealerMinigame( game: Game, ) : Minigame(game, "damagedealer") { private val levelItems = mutableListOf() @@ -174,14 +178,17 @@ class DamageDealer( entity.isCustomNameVisible = true } + override fun onLoad() { + game.world.setGameRule(GameRule.NATURAL_REGENERATION, false) + super.onLoad() + } + override fun start() { super.start() - startPos.world.setGameRule(GameRule.NATURAL_REGENERATION, false) val posSpider = startPos.clone().add(2.0, 0.0, 0.0) val posZombie = startPos.clone().add(-2.0, 0.0, 0.0) spawnTarget(posSpider, Spider::class.java) - val zombie = spawnTarget(posZombie, Zombie::class.java) - zombie.equipment.helmet = ItemStack.of(Material.IRON_HELMET) + spawnTarget(posZombie, Zombie::class.java) game.onlinePlayers.forEach { player -> giveItems(player, 0) } @@ -192,7 +199,7 @@ class DamageDealer( "- You only have 20 tries!" audience.sendMessage(mm.deserialize(introductionMessage)) - startCountdown(3 * 60 * 1000) { + startCountdown(3 * 60 * 20) { end() } } @@ -203,15 +210,16 @@ class DamageDealer( } override fun handleEntityDamageByEntity(event: EntityDamageByEntityEvent) { - if (event.entity is Player) { - event.isCancelled = true - } val damage = event.finalDamage event.damage = 0.0 val player = event.damager if (player !is Player) { return } + val target = event.entity + if (target is LivingEntity) { + target.noDamageTicks = 0 + } val level = 20 - player.health.toInt() val multiplier = 1.5 - level * 0.02 val finalScore = floor(damage * multiplier).toInt() @@ -226,8 +234,8 @@ class DamageDealer( } audience.sendMessage(mm.deserialize("${player.name} has finished!")) } else { - player.damage(1.0) - giveItems(player, 20 - player.health.toInt()) + player.health -= 1 + giveItems(player, level + 1) } } @@ -235,12 +243,10 @@ class DamageDealer( event.isCancelled = false } - override val name: Component - get() = Component.text("Damage Dealer", NamedTextColor.AQUA) - override val description: Component - get() = - Component.text( - "Deal as much damage as possible!\nAfter each hit, you lose 1 health and get new items!", - NamedTextColor.AQUA, - ) + override val name = Component.text("Damage Dealer", NamedTextColor.AQUA) + override val description = + Component.text( + "Deal as much damage as possible!\nAfter each hit, you lose 1 health and get new items!", + NamedTextColor.AQUA, + ) } diff --git a/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt similarity index 96% rename from src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt index 0ee8496..e4411c2 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt @@ -2,6 +2,9 @@ package info.mester.network.partygames.game import com.destroystokyo.paper.ParticleBuilder import info.mester.network.partygames.UUIDDataType +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.GameState +import info.mester.network.partygames.api.Minigame import info.mester.network.partygames.game.gardening.Cactus import info.mester.network.partygames.game.gardening.GardenTap import info.mester.network.partygames.game.gardening.Lilac @@ -35,7 +38,6 @@ import org.bukkit.entity.Player import org.bukkit.entity.Rabbit import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.block.BlockPhysicsEvent -import org.bukkit.event.entity.EntityCombustEvent import org.bukkit.event.entity.PlayerDeathEvent import org.bukkit.event.player.PlayerDropItemEvent import org.bukkit.event.player.PlayerInteractEvent @@ -61,6 +63,7 @@ private enum class ObjectType { LILAC, } +@Suppress("Unused") class GardeningMinigame( game: Game, ) : Minigame(game, "gardening") { @@ -85,8 +88,8 @@ class GardeningMinigame( // set the player's xp progress to the tap's water level player.exp = tap.getFullness().toFloat() val power = hosePowers[player.uniqueId]!! - // power goes from 0 to 10, so we divide by 10 to get a value between 0 and 1 - val normalPower = power / 10.0 + // power goes from 0 to 10 + val normalPower = power / 6.5 val direction = player.location.direction .normalize() @@ -254,13 +257,17 @@ class GardeningMinigame( private fun getPlayersFromTap(tap: GardenTap): List = game.onlinePlayers.filter { getTap(it) == tap } - override fun start() { - super.start() - fetchAllGrassBlocks() - val worldBorder = startPos.world.worldBorder + override fun onLoad() { + val worldBorder = game.world.worldBorder worldBorder.center = startPos worldBorder.size = 2 * MAP_RADIUS + 1.0 worldBorder.warningDistance = 0 + super.onLoad() + } + + override fun start() { + super.start() + fetchAllGrassBlocks() // start a timer that triggers the hose shooting Bukkit.getScheduler().runTaskTimer(plugin, { t -> if (!running) { @@ -311,8 +318,8 @@ class GardeningMinigame( val spawnedObject = listOf( WeightedItem(ObjectType.WEED, 25), - WeightedItem(ObjectType.ZOMBIE_WEED, 2), - WeightedItem(ObjectType.RAINBOW_FLOWER, 5), + WeightedItem(ObjectType.ZOMBIE_WEED, 5), + WeightedItem(ObjectType.RAINBOW_FLOWER, 1), WeightedItem(ObjectType.OAK_TREE, 15), WeightedItem(ObjectType.CACTUS, 35), WeightedItem(ObjectType.SUNFLOWER, 80), @@ -349,7 +356,7 @@ class GardeningMinigame( } }, 0, 5) // start a 2,5-minute countdown for the game - startCountdown((2.5 * 60 * 1000).toLong()) { + startCountdown((2.5 * 60 * 20).toInt()) { end() } // setup players @@ -539,17 +546,11 @@ class GardeningMinigame( } } - override fun handleEntityCombust(event: EntityCombustEvent) { - if (event.entity.type == EntityType.ZOMBIE) { - event.isCancelled = true - } - } - override fun handlePlayerDeath(event: PlayerDeathEvent) { event.isCancelled = true val player = event.player player.teleport(startPos) - game.addScore(player, -100, "dying... seriously, how?") + game.addScore(player, -50, "dying... seriously, how?") } override fun handleBlockBreak(event: BlockBreakEvent) { @@ -567,8 +568,6 @@ class GardeningMinigame( } } - override val name: Component - get() = Component.text("Gardening", NamedTextColor.AQUA) - override val description: Component - get() = Component.text("Use your tools to grow plants and remove weeds!", NamedTextColor.AQUA) + override val name = Component.text("Gardening", NamedTextColor.AQUA) + override val description = Component.text("Use your tools to grow plants and remove weeds!", NamedTextColor.AQUA) } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GravjumpMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GravjumpMinigame.kt new file mode 100644 index 0000000..31da32b --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GravjumpMinigame.kt @@ -0,0 +1,319 @@ +package info.mester.network.partygames.game + +import com.sk89q.worldedit.bukkit.BukkitAdapter +import com.sk89q.worldedit.extent.clipboard.io.BuiltInClipboardFormat +import com.sk89q.worldedit.math.transform.AffineTransform +import com.sk89q.worldedit.regions.CuboidRegion +import info.mester.network.partygames.PartyGames +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.IntroductionType +import info.mester.network.partygames.api.Minigame +import io.papermc.paper.entity.TeleportFlag +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Bukkit +import org.bukkit.GameRule +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.block.structure.Mirror +import org.bukkit.block.structure.StructureRotation +import org.bukkit.configuration.file.YamlConfiguration +import org.bukkit.event.player.PlayerMoveEvent +import org.bukkit.event.player.PlayerTeleportEvent +import org.bukkit.structure.Structure +import org.bukkit.util.Vector +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.Random +import java.util.UUID +import kotlin.math.cos +import kotlin.math.sin + +/** + * The current gravity direction. + * + * Visually it represents which original direction is "down" for the player if looking in the positive X direction. + */ +private enum class Gravity( + val rotateAmount: Double, +) { + DOWN(0.0), + UP(180.0), + LEFT(90.0), + RIGHT(270.0), ; + + fun relativeTo(other: Gravity): Gravity { + val rotationChange = (other.rotateAmount - this.rotateAmount + 360.0) % 360.0 + return entries.firstOrNull { it.rotateAmount == rotationChange } ?: DOWN + } +} + +private data class RegisteredSection( + val structure: Structure, + val file: File, + val rotated: MutableMap = mutableMapOf(), +) + +class GravjumpMinigame( + game: Game, +) : Minigame(game, "gravjump", introductionType = IntroductionType.STATIC) { + companion object { + const val SECTION_WIDTH = 15 + const val SECTION_LENGTH = 32 + const val SECTIONS_PER_GAME = 10 + + private val plugin = PartyGames.plugin + lateinit var start: Vector + private set + lateinit var wallFrom: Vector + private set + lateinit var wallTo: Vector + private set + lateinit var sectionStart: Vector + private set + private val sections = mutableListOf() + + fun reload() { + val config = YamlConfiguration.loadConfiguration(File(plugin.dataFolder, "gravjump.yml")) + plugin.logger.info("Loading gravjump config...") + plugin.saveResource("gravjump/start.nbt", true) + + // load positions + val startConfig = config.getConfigurationSection("start") + start = + Vector( + startConfig?.getDouble("x") ?: 0.0, + startConfig?.getDouble("y") ?: 0.0, + startConfig?.getDouble("z") ?: 0.0, + ) + val wallFromConfig = config.getConfigurationSection("wall-from") + wallFrom = + Vector( + wallFromConfig?.getDouble("x") ?: 0.0, + wallFromConfig?.getDouble("y") ?: 0.0, + wallFromConfig?.getDouble("z") ?: 0.0, + ) + val wallToConfig = config.getConfigurationSection("wall-to") + wallTo = + Vector( + wallToConfig?.getDouble("x") ?: 0.0, + wallToConfig?.getDouble("y") ?: 0.0, + wallToConfig?.getDouble("z") ?: 0.0, + ) + val sectionStartConfig = config.getConfigurationSection("section-start") + sectionStart = + Vector( + sectionStartConfig?.getDouble("x") ?: 0.0, + sectionStartConfig?.getDouble("y") ?: 0.0, + sectionStartConfig?.getDouble("z") ?: 0.0, + ) + + // load sections + val sectionList = config.getStringList("sections") + sections.clear() + for (sectionName in sectionList) { + val structurePath = "gravjump/$sectionName.nbt" + + val structureInJar = plugin.getResource(structurePath) != null + if (structureInJar) { + plugin.saveResource(structurePath, true) + } + + val structureFile = File(plugin.dataFolder, structurePath) + if (!structureFile.exists()) { + plugin.logger.warning("Structure file $structurePath does not exist!") + continue + } + + val section = + RegisteredSection( + Bukkit.getStructureManager().loadStructure(structureFile), + structureFile, + ) + setupSectionRotations(section) + + sections.add(section) + } + } + + private fun setupSectionRotations(section: RegisteredSection) { + section.rotated.clear() + section.rotated[Gravity.DOWN] = section.structure + + for (gravity in Gravity.entries.filterNot { it == Gravity.DOWN }) { + val format = BuiltInClipboardFormat.MINECRAFT_STRUCTURE + val clipboard = + format.getReader(FileInputStream(section.file)).use { + it.read() + } + + // rotate according to the gravity + val rotate = AffineTransform().rotateX(-gravity.rotateAmount) + val target = clipboard.transform(rotate) + + val tempFile = File(plugin.dataFolder, "gravjump/temp-${UUID.randomUUID()}-$gravity.nbt") + format.getWriter(FileOutputStream(tempFile)).use { writer -> + writer.write(target) + } + + val rotatedStructure = Bukkit.getStructureManager().loadStructure(tempFile) + section.rotated[gravity] = rotatedStructure + + tempFile.delete() + } + } + } + + private val placedSections = mutableListOf() + private var gravity = Gravity.DOWN + + private fun setupMap() { + val startStructure = + Bukkit.getStructureManager().loadStructure( + File(originalPlugin.dataFolder, "gravjump/start.nbt"), + ) + startStructure.place( + start.clone().toLocation(startPos.world), + true, + StructureRotation.NONE, + Mirror.NONE, + 0, + 1f, + Random(), + ) + + for (i in 0 until SECTIONS_PER_GAME) { + // randomly select a section from the list + val section = sections.randomOrNull() ?: continue + placedSections.add(section) + + val sectionLocation = + sectionStart.clone().toLocation(startPos.world).add(i * SECTION_LENGTH.toDouble(), 0.0, 0.0) + section.structure.place( + sectionLocation, + true, + StructureRotation.NONE, + Mirror.NONE, + 0, + 1f, + Random(), + ) + } + } + + override fun onLoad() { + setupMap() + startPos.world.setGameRule(GameRule.DO_TILE_DROPS, false) + super.onLoad() + } + + override fun start() { + super.start() + + startCountdown(5 * 20) { + // remove the wall + val wallStart = BukkitAdapter.asBlockVector(wallFrom.toLocation(startPos.world)) + val wallEnd = BukkitAdapter.asBlockVector(wallTo.toLocation(startPos.world)) + val wall = CuboidRegion(wallStart, wallEnd) + for (vec in wall) { + val location = Location(startPos.world, vec.x().toDouble(), vec.y().toDouble(), vec.z().toDouble()) + location.block.type = Material.AIR + } + + Bukkit.getScheduler().runTaskTimer(plugin, { t -> + if (!running) { + t.cancel() + return@runTaskTimer + } + + val newGravity = Gravity.entries.filter { it != gravity }.random() + audience.sendMessage( + Component.text( + "Gravity is about to change! Your ${ + gravity.relativeTo( + newGravity, + ).name.lowercase() + } will be your new down in 3 seconds!", + NamedTextColor.GRAY, + ), + ) + startCountdown(3 * 20, false) { + flip(newGravity) + } + }, 4 * 20, 20 * 20) + } + } + + /** + * Flips the entire map according to the current gravity direction. + */ + private fun flip(new: Gravity? = null) { + // first select a random gravity direction + val originalGravity = gravity + gravity = new ?: Gravity.entries.filter { it != gravity }.random() + + for (i in 0 until SECTIONS_PER_GAME) { + val rotatedStructure = placedSections[i].rotated[gravity] ?: continue + val sectionLocation = + sectionStart.clone().toLocation(startPos.world).add(i * SECTION_LENGTH.toDouble(), 0.0, 0.0) + + rotatedStructure.place( + sectionLocation, + true, + StructureRotation.NONE, + Mirror.NONE, + 0, + 1f, + Random(), + ) + } + + // finally let's rotate players + for (player in game.onlinePlayers) { + // imagine z and y as the x,y coordinates on a 2d plane + // we'll now rotate it with the origin being this imaginary x-axis + val y = player.location.y - (sectionStart.y + SECTION_WIDTH / 2.0) + val z = player.location.z - (sectionStart.z + SECTION_WIDTH / 2.0) + + val degrees = gravity.rotateAmount - originalGravity.rotateAmount + val radians = Math.toRadians(degrees) + val cos = cos(radians) + val sin = sin(radians) + val newZ = z * cos - y * sin + val newY = z * sin + y * cos + + val finalLocation = + player.location.clone().apply { + setY((sectionStart.y + SECTION_WIDTH / 2.0) + newY) + setZ((sectionStart.z + SECTION_WIDTH / 2.0) + newZ) + } + player.teleportAsync( + finalLocation, + PlayerTeleportEvent.TeleportCause.PLUGIN, + TeleportFlag.Relative.VELOCITY_X, + TeleportFlag.Relative.VELOCITY_Y, + TeleportFlag.Relative.VELOCITY_Z, + ) + } + } + + fun flip() { + flip(null) + } + + override fun handlePlayerMove(event: PlayerMoveEvent) { + if (event.to.y <= sectionStart.y + 1.2) { + event.to = startPos + } + super.handlePlayerMove(event) + } + + override val name: Component = Component.text("Gravjump", NamedTextColor.AQUA) + override val description: Component = + Component.text( + "Reach the end of a randomly generated course as fast as possible with the help of abilities.\n" + + "Watch out, your down may change at any time!", + NamedTextColor.AQUA, + ) +} diff --git a/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt similarity index 66% rename from src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index 8abe49c..627fccb 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -2,13 +2,17 @@ package info.mester.network.partygames.game import com.destroystokyo.paper.ParticleBuilder import info.mester.network.partygames.PartyGames +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.Minigame import info.mester.network.partygames.game.healthshop.HealthShopItem +import info.mester.network.partygames.game.healthshop.HealthShopPlayerData import info.mester.network.partygames.game.healthshop.HealthShopUI import info.mester.network.partygames.game.healthshop.SupplyChestTimer import info.mester.network.partygames.mm import info.mester.network.partygames.util.WeightedItem import info.mester.network.partygames.util.selectWeightedRandom import info.mester.network.partygames.util.spreadPlayers +import io.papermc.paper.datacomponent.DataComponentTypes import io.papermc.paper.event.player.PrePlayerAttackEntityEvent import net.kyori.adventure.key.Key import net.kyori.adventure.sound.Sound @@ -26,13 +30,16 @@ import org.bukkit.block.Chest import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.damage.DamageSource import org.bukkit.damage.DamageType -import org.bukkit.enchantments.Enchantment import org.bukkit.entity.EntityType import org.bukkit.entity.FallingBlock +import org.bukkit.entity.Fireball import org.bukkit.entity.Player +import org.bukkit.entity.TNTPrimed import org.bukkit.event.Event +import org.bukkit.event.block.BlockPhysicsEvent import org.bukkit.event.block.BlockPlaceEvent import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent import org.bukkit.event.entity.EntityRegainHealthEvent import org.bukkit.event.entity.EntityShootBowEvent import org.bukkit.event.entity.PlayerDeathEvent @@ -43,17 +50,21 @@ import org.bukkit.event.player.PlayerItemConsumeEvent import org.bukkit.event.player.PlayerMoveEvent import org.bukkit.event.player.PlayerToggleFlightEvent import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.ItemType import org.bukkit.inventory.meta.CompassMeta +import org.bukkit.inventory.meta.FireworkMeta import org.bukkit.persistence.PersistentDataType -import org.bukkit.scheduler.BukkitTask +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import org.bukkit.scheduler.BukkitRunnable import org.bukkit.util.Vector import java.io.File import java.util.UUID import java.util.concurrent.TimeUnit -import java.util.function.Consumer import java.util.logging.Level import kotlin.math.floor import kotlin.math.max +import kotlin.math.min import kotlin.random.Random enum class HealthShopMinigameState { @@ -62,28 +73,46 @@ enum class HealthShopMinigameState { FIGHT, } +private data class StartLocation( + val vector: Vector, + val yaw: Float, + val pitch: Float, +) { + fun toLocation(world: org.bukkit.World) = + vector.toLocation(world).apply { + this.yaw = yaw + this.pitch = pitch + } +} + class ShopFailedException( message: String, ) : Exception(message) class HealthShopMinigame( game: Game, -) : Minigame(game, "healthshop") { +) : Minigame(game, "healthshop", allowFallDamage = true) { companion object { private val shopItems: MutableList = mutableListOf() - private val startLocations: MutableMap> = mutableMapOf() + private val startLocations: MutableMap> = mutableMapOf() private val supplyDrops: MutableList> = mutableListOf() - private var startingHealth: Double = 80.0 + private val gameTimes = mutableMapOf() + var startingHealth: Double = 80.0 + private set private val plugin = PartyGames.plugin + fun getShopItems(): List = shopItems.toList() + init { reload() } fun reload() { val config = YamlConfiguration.loadConfiguration(File(plugin.dataFolder, "health-shop.yml")) - // load shop items by obtaining the config and reading every key inside "items" of "health-shop.yml" - plugin.logger.info("Loading shop items...") + plugin.logger.info("Loading Health Shop config...") + + // load shop items + shopItems.clear() config.getConfigurationSection("items")?.getKeys(false)?.forEach { key -> try { val shopItem = HealthShopItem.loadFromConfig(config.getConfigurationSection("items.$key")!!, key) @@ -93,22 +122,28 @@ class HealthShopMinigame( plugin.logger.log(Level.WARNING, e.message, e) } } + // load spawn locations - plugin.logger.info("Loading spawn locations...") - val spawnLocationConfig = config.getConfigurationSection("spawn-locations")!! - spawnLocationConfig.getKeys(false).forEach { key -> + startLocations.clear() + config.getConfigurationSection("spawn-locations")?.getKeys(false)?.forEach { key -> try { // try to convert the key to an integer val id = key.toIntOrNull() ?: return@forEach // now load all the spawn locations - val locationList = spawnLocationConfig.getList(key) ?: return@forEach + val locationList = config.getList("spawn-locations.$key") ?: return@forEach val locations = locationList.mapNotNull { entry -> if (entry is Map<*, *>) { val x = entry["x"] as? Double ?: return@mapNotNull null val y = entry["y"] as? Double ?: return@mapNotNull null val z = entry["z"] as? Double ?: return@mapNotNull null - Vector(x, y, z) + val yaw = entry["yaw"] as? Double ?: 0.0 + val pitch = entry["pitch"] as? Double ?: 0.0 + StartLocation( + Vector(x, y, z), + yaw.toFloat(), + pitch.toFloat(), + ) } else { null } @@ -119,6 +154,20 @@ class HealthShopMinigame( plugin.logger.log(Level.WARNING, e.message, e) } } + + // load game times + gameTimes.clear() + config.getConfigurationSection("game-times")?.getKeys(false)?.forEach { map -> + try { + val time = config.getInt("game-times.$map") + gameTimes[map] = time + } catch (e: Exception) { + plugin.logger.warning("Failed to load game time $map") + plugin.logger.log(Level.WARNING, e.message, e) + } + } + + // load supply drops supplyDrops.clear() config.getList("supply-drops")?.forEach { entry -> if (entry is Map<*, *>) { @@ -127,26 +176,29 @@ class HealthShopMinigame( supplyDrops.add(WeightedItem(key, weight)) } } + startingHealth = config.getDouble("health", 80.0) } } private val arrowRegenerating = mutableListOf() private var state = HealthShopMinigameState.STARTING - private var fightStartedTime = -1L + private var fightStartedTime = -1 private var readyPlayers = 0 /* * A map that links player UUIDs to their last damage source's UUID and a Long representing the time they were damaged */ private val lastDamageTimes = mutableMapOf>() - private val lastDoubleJump = mutableMapOf() + private val lastDoubleJump = mutableMapOf() /** - * Every shop UI associated with a player + * Every shop associated with a player */ - private val shopUIs: Map = - onlinePlayers.map { it.uniqueId }.associateWith { HealthShopUI(it, shopItems, startingHealth) } + private val shops: Map = + game.players.map { it.uniqueId }.associateWith { HealthShopUI(it, startingHealth) } + + private fun getPlayerData(player: Player): HealthShopPlayerData = shops[player.uniqueId]?.playerData ?: HealthShopPlayerData() private fun regenerateArrowTimer( player: Player, @@ -169,13 +221,13 @@ class HealthShopMinigame( // check if the countdown is over if (timeRemaining <= 0) { // count the arrows in the player's inventory - var needsArrows = !player.inventory.contains(Material.ARROW, HealthShopUI.maxArrows(player.uniqueId)) + var needsArrows = !player.inventory.contains(Material.ARROW, getPlayerData(player).maxArrows) if (!needsArrows) { return false } // give the player an arrow player.inventory.addItem(ItemStack.of(Material.ARROW, 1)) - needsArrows = !player.inventory.contains(Material.ARROW, HealthShopUI.maxArrows(player.uniqueId)) + needsArrows = !player.inventory.contains(Material.ARROW, getPlayerData(player).maxArrows) // if the player still needs more arrows, start the timer again if (needsArrows) { Bukkit.getScheduler().runTaskLater(plugin, Runnable { regenerateArrow(player) }, 1) @@ -212,25 +264,33 @@ class HealthShopMinigame( player: Player, didSurvive: Boolean, ) { + if (fightStartedTime == -1) { + return + } + val survivedTicks = Bukkit.getCurrentTick() - fightStartedTime + val survivedSeconds = survivedTicks / 20 // for every 20th second the player has survived, give them a point - val survivedTime = System.currentTimeMillis() - fightStartedTime - val survivedSeconds = survivedTime / 1000 - val survivedPoints = floor((survivedSeconds / 20).toDouble()).toInt() * (if (didSurvive) 2 else 1) + // 1 point every 10th second if the player is still alive (last player standing, time is up) + val survivedPoints = floor(survivedSeconds / if (didSurvive) 10.0 else 20.0).toInt() if (survivedPoints > 0) { game.addScore(player, survivedPoints, "Survived $survivedSeconds seconds") } } - override fun start() { - super.start() - startPos.world.time = 13000 + override fun onLoad() { + game.world.time = 13000 // set up the world border - val worldBorder = startPos.world.worldBorder + val worldBorder = game.world.worldBorder worldBorder.size = 121.0 worldBorder.center = startPos worldBorder.warningDistance = 2 worldBorder.damageBuffer = 0.0 worldBorder.damageAmount = 1.5 + super.onLoad() + } + + override fun start() { + super.start() // send the players to the predefined spawn locations val spawnLocations = if (startLocations.contains(worldIndex)) startLocations[worldIndex]!!.toList().shuffled() else emptyList() @@ -248,7 +308,7 @@ class HealthShopMinigame( state = HealthShopMinigameState.SHOP for (player in game.onlinePlayers) { // open the shop UI for all players - player.openInventory(shopUIs[player.uniqueId]!!.inventory) + player.openInventory(shops[player.uniqueId]!!.inventory) // prevent players from glitching out when spawned on a slab or otherwise non-full block player.allowFlight = true player.isFlying = true @@ -256,25 +316,9 @@ class HealthShopMinigame( setMaxHealth(player, startingHealth) player.health = startingHealth player.sendHealthUpdate() - // reset perks - player.persistentDataContainer.set( - NamespacedKey(plugin, "steal_perk"), - PersistentDataType.BOOLEAN, - false, - ) - player.persistentDataContainer.set( - NamespacedKey(plugin, "heal_perk"), - PersistentDataType.BOOLEAN, - false, - ) - player.persistentDataContainer.set( - NamespacedKey(plugin, "double_jump"), - PersistentDataType.BOOLEAN, - false, - ) } - // start a 30-second countdown for the shop state - startCountdown(30000) { + // start a countdown for the shop state + startCountdown(45 * 20) { startFight() } } @@ -285,7 +329,8 @@ class HealthShopMinigame( } state = HealthShopMinigameState.FIGHT - fightStartedTime = System.currentTimeMillis() + fightStartedTime = Bukkit.getCurrentTick() + val gameTime = gameTimes[rootWorld.name] ?: (3 * 60) // default to 3 minutes if not specified for (player in game.onlinePlayers) { // close the shop UI @@ -300,21 +345,60 @@ class HealthShopMinigame( player.sendHealthUpdate() // time to give the items! :) player.inventory.clear() - shopUIs[player.uniqueId]!!.giveItems() + shops[player.uniqueId]!!.giveItems() + // if we're on the urban map, make every armor piece act as an elytra + if (rootWorld.name == "mg-healthshop4") { + @Suppress("UnstableApiUsage") + player.inventory.chestplate?.apply { + val elytraEquip = ItemType.ELYTRA.getDefaultData(DataComponentTypes.EQUIPPABLE) ?: return@apply + setData(DataComponentTypes.EQUIPPABLE, elytraEquip) + setData(DataComponentTypes.GLIDER) + } + val firework = + ItemStack.of(Material.FIREWORK_ROCKET, 8).apply { + editMeta(FireworkMeta::class.java) { meta -> + meta.power = 6 + } + } + player.inventory.addItem(firework) + } // give the actual arrow items based on maxArrows - val maxArrows = HealthShopUI.maxArrows(player.uniqueId) + val maxArrows = getPlayerData(player).maxArrows if (maxArrows > 0) { player.inventory.addItem(ItemStack.of(Material.ARROW, maxArrows)) } } - // start a 3-minute countdown for the fight - startCountdown(3 * 60 * 1000) { + + // start the countdown for the end + startCountdown(gameTime * 20) { end() } + // start the supply chest timer - Bukkit.getScheduler().runTaskTimer(plugin, SupplyChestTimer(this, 3 * 60 * 20), 0, 1) - // shrink the world border to completely close in the last 30 seconds (5 minutes is the fight duration) - startPos.world.worldBorder.setSize(5.0, TimeUnit.SECONDS, 3 * 60 - 30) + Bukkit.getScheduler().runTaskTimer(plugin, SupplyChestTimer(this, gameTime * 20), 0, 1) + + // shrink the world border to close to 3 blocks in the last 20 seconds + startPos.world.worldBorder.setSize(3.0, TimeUnit.SECONDS, gameTime - 20L) + + // randomly move the world border in the last 20 seconds every 2.5 seconds + Bukkit.getScheduler().runTaskTimer( + plugin, + { t -> + if (!running) { + t.cancel() + return@runTaskTimer + } + + val worldBorder = startPos.world.worldBorder + val x = worldBorder.center.x + Random.nextInt(-3, 3) + val z = worldBorder.center.z + Random.nextInt(-3, 3) + worldBorder.setCenter(x, z) + worldBorder.damageAmount = 4.5 + worldBorder.damageBuffer = 0.0 + }, + (gameTime) * 20 + 50L, + 50, + ) } override fun finish() { @@ -326,6 +410,7 @@ class HealthShopMinigame( // reset max health val attribute = player.getAttribute(Attribute.MAX_HEALTH)!! attribute.baseValue = attribute.defaultValue + player.sendHealthUpdate() } } @@ -334,13 +419,18 @@ class HealthShopMinigame( val worldBorder = startPos.world.worldBorder // generate a random location within the world border (minus 2 blocks to avoid spawning on the border) val maxSize = (worldBorder.size.toInt() - 2) / 2 - var x: Double - var z: Double - while (true) { + var x: Double = worldBorder.center.x + var z: Double = worldBorder.center.z + var attempts = 0 + while (attempts < 5) { + attempts++ x = worldBorder.center.x + Random.nextInt(-maxSize, maxSize) z = worldBorder.center.z + Random.nextInt(-maxSize, maxSize) // check if the location is not facing the void - if (startPos.world.getHighestBlockAt(x.toInt(), z.toInt()).type != Material.AIR) { + if (!startPos.world + .getHighestBlockAt(x.toInt(), z.toInt()) + .type.isEmpty + ) { break } } @@ -401,7 +491,7 @@ class HealthShopMinigame( ) } - fun handleEntityShootBow(event: EntityShootBowEvent) { + override fun handleEntityShootBow(event: EntityShootBowEvent) { if (state != HealthShopMinigameState.FIGHT) { return } @@ -453,13 +543,33 @@ class HealthShopMinigame( event.player.sendMessage(Component.text("Left click to reopen the shop.", NamedTextColor.AQUA)) } + @Suppress("UnstableApiUsage") + override fun handleEntityDamage(event: EntityDamageEvent) { + val player = event.entity as? Player ?: return + val damageType = event.damageSource.damageType + if (damageType == DamageType.FALL && getPlayerData(player).featherFall) { + event.isCancelled = true + } + if ((damageType == DamageType.EXPLOSION || damageType == DamageType.PLAYER_EXPLOSION) && getPlayerData(player).blastProtection) { + event.isCancelled = true + } + super.handleEntityDamage(event) + } + + @Suppress("UnstableApiUsage") override fun handleEntityDamageByEntity(event: EntityDamageByEntityEvent) { if (state != HealthShopMinigameState.FIGHT) { return } - val damager = event.damager - val damagee = event.entity - if (damagee !is Player || damager !is Player) { + + // limit fireball damage to 1 heart + if (event.damageSource.directEntity is Fireball) { + event.damage = min(2.0, event.damage) + } + + val damagee = event.entity as? Player ?: return + val damager = event.damageSource.causingEntity + if (damager !is Player) { return } @@ -523,11 +633,7 @@ class HealthShopMinigame( if (killerPlayer is Player) { game.addScore(killerPlayer, 40, "Killed ${event.entity.name}") // check if the player has the steal perk - if (killerPlayer.persistentDataContainer.get( - NamespacedKey(plugin, "steal_perk"), - PersistentDataType.BOOLEAN, - ) == true - ) { + if (getPlayerData(killerPlayer).stealPerk) { // copy the inventory of the killed player for (i in 0..40) { killedPlayer.inventory.getItem(i)?.let { item -> @@ -536,11 +642,7 @@ class HealthShopMinigame( } } // check if the player has the heal perk - if (killerPlayer.persistentDataContainer.get( - NamespacedKey(plugin, "heal_perk"), - PersistentDataType.BOOLEAN, - ) == true - ) { + if (getPlayerData(killerPlayer).healPerk) { killerPlayer.health = killerPlayer.getAttribute(Attribute.MAX_HEALTH)!!.value killerPlayer.sendHealthUpdate() } @@ -551,7 +653,7 @@ class HealthShopMinigame( } } - fun handleEntityRegainHealth(event: EntityRegainHealthEvent) { + override fun handleEntityRegainHealth(event: EntityRegainHealthEvent) { // don't let players during the shop state regain health if (state == HealthShopMinigameState.SHOP) { event.isCancelled = true @@ -559,19 +661,22 @@ class HealthShopMinigame( } } - fun handlePlayerItemConsume(event: PlayerItemConsumeEvent) { + override fun handlePlayerItemConsume(event: PlayerItemConsumeEvent) { if (state != HealthShopMinigameState.FIGHT) { return } val item = event.item - // check if the item has a special golden_apple_inf PDC - if (item.itemMeta.hasEnchant(Enchantment.LUCK_OF_THE_SEA)) { - // add a cooldown to the enchanted golden apple (10 seconds) - event.player.setCooldown(Material.ENCHANTED_GOLDEN_APPLE, 10 * 20) - } if (item.type == Material.POTION) { event.replacement = ItemStack.of(Material.AIR) } + if (item.type == Material.GOLDEN_APPLE) { + @Suppress("UnstableApiUsage") + val isInfinite = item.getData(DataComponentTypes.USE_COOLDOWN) != null + + if (isInfinite) { + event.replacement = item.clone() + } + } } override fun handlePlayerInteract(event: PlayerInteractEvent) { @@ -583,11 +688,11 @@ class HealthShopMinigame( readyPlayers = readyPlayers.coerceAtLeast(0) sendReadyStatus() // open the shop UI - event.player.openInventory(shopUIs[event.player.uniqueId]!!.inventory) + event.player.openInventory(shops[event.player.uniqueId]!!.inventory) } else if (state == HealthShopMinigameState.FIGHT) { // check if item is the tracker - val item = event.item - if (item?.type == Material.COMPASS) { + val item = event.item ?: return + if (item.type == Material.COMPASS) { val player = event.player if (player.hasCooldown(Material.COMPASS)) { return @@ -596,10 +701,9 @@ class HealthShopMinigame( event.setUseItemInHand(Event.Result.DENY) // get the nearest player val nearestPlayer = - Bukkit - .getOnlinePlayers() + game.onlinePlayers .filter { - it.gameMode == GameMode.SURVIVAL && !PartyGames.plugin.isAdmin(it) && it.uniqueId != player.uniqueId + it.gameMode == GameMode.SURVIVAL && it.uniqueId != player.uniqueId }.minByOrNull { it.location.distance(player.location) } if (nearestPlayer == null) { player.sendMessage( @@ -612,6 +716,16 @@ class HealthShopMinigame( } player.setCooldown(Material.COMPASS, 5 * 20) nearestPlayer.sendMessage(Component.text("You have been tracked!", NamedTextColor.GREEN)) + // apply 20 seconds of glowing effect to the nearest player + nearestPlayer.addPotionEffect( + PotionEffect( + PotionEffectType.GLOWING, + 20 * 20, + 0, + false, + false, + ), + ) // set the compass' direction to the nearest player's location item.editMeta { meta -> val compassMeta = meta as CompassMeta @@ -619,6 +733,22 @@ class HealthShopMinigame( compassMeta.isLodestoneTracked = false } } + if (item.type == Material.FIRE_CHARGE && event.action.isRightClick && !event.player.hasCooldown(Material.FIRE_CHARGE)) { + event.setUseInteractedBlock(Event.Result.DENY) + event.setUseItemInHand(Event.Result.DENY) + item.amount -= 1 + + // launch a fireball in the direction the player is looking + val fireball = event.player.launchProjectile(Fireball::class.java) + fireball.setIsIncendiary(false) + fireball.yield = 4f + fireball.velocity = + event.player.location.direction + .multiply(0.8) + + // 2.5 seconds cooldown + event.player.setCooldown(Material.FIRE_CHARGE, 50) + } } } @@ -630,58 +760,96 @@ class HealthShopMinigame( event.to.z = event.from.z return } - // check for double jump perk + + // check if the player is on the ground and allow flight (to trigger double jump) val player = event.player - if (player.persistentDataContainer.get( - NamespacedKey(PartyGames.plugin, "double_jump"), - PersistentDataType.BOOLEAN, - ) == true && - player.gameMode != GameMode.CREATIVE + if (getPlayerData(player).doubleJump && + player.gameMode != GameMode.CREATIVE && + state == HealthShopMinigameState.FIGHT && + player.location.block + .getRelative(BlockFace.DOWN) + .isSolid ) { - // check if the player is on the ground and allow - if (player.location.block - .getRelative(BlockFace.DOWN) - .type != Material.AIR - ) { - player.allowFlight = true - } + player.allowFlight = true } + // check if player is below 0 (kill instantly) if (player.location.y < 0) { player.teleport(startPos) @Suppress("UnstableApiUsage") - player.damage(9999.0, DamageSource.builder(DamageType.OUT_OF_WORLD).build()) + player.damage( + 9999.0, + DamageSource.builder(DamageType.OUT_OF_WORLD).build(), + ) + } + + // show warning particles when player is below 10 + if (player.location.y < 10 && Bukkit.getCurrentTick() % 3 == 0) { + for (x in -5..5) { + for (z in -5..5) { + val location = player.location.clone().add(x.toDouble(), 0.0, z.toDouble()) + location.y = 0.0 + ParticleBuilder(Particle.DUST) + .location(location) + .color(255, 0, 0) + .count(1) + .receivers(player) + .spawn() + } + } } } override fun handleBlockPlace(event: BlockPlaceEvent) { - if (event.block.type == Material.OAK_PLANKS) { - // start a nice animation that eventually breaks the block - Bukkit.getScheduler().runTaskTimer( - plugin, - object : Consumer { - private var remainingTime = 6 * 20 + val block = event.block + if (block.type == Material.OAK_PLANKS) { + val location = block.location.clone() + val world = block.world + val totalTime = 6 * 20 // 6 seconds in ticks + + object : BukkitRunnable() { + var remainingTime = totalTime + + override fun run() { + if (!running) { + cancel() + return + } - override fun accept(t: BukkitTask) { - if (!running) { - t.cancel() - return - } - remainingTime -= 1 - if (remainingTime <= 0) { - event.block.type = Material.AIR - t.cancel() - return - } else { - // calculate the progress - val progress = 1 - (remainingTime.toFloat() / (6 * 20)) - game.onlinePlayers.forEach { it.sendBlockDamage(event.block.location, progress) } - } + // Check if the block is still the expected type + if (world.getBlockAt(location).type != Material.OAK_PLANKS) { + cancel() + return } - }, - 0, - 1, - ) + + remainingTime-- + if (remainingTime <= 0) { + world.getBlockAt(location).type = Material.AIR + cancel() + return + } + + val progress = 1 - (remainingTime.toFloat() / totalTime) + for (player in game.onlinePlayers) { + player.sendBlockDamage(location, progress) + } + } + }.runTaskTimer(plugin, 0, 1) + } + if (block.type == Material.TNT) { + block.type = Material.AIR + // spawn a primed tnt + block.world.spawn(block.location.clone().add(0.5, 0.0, 0.5), TNTPrimed::class.java) { tnt -> + tnt.source = event.player + tnt.fuseTicks = 60 // 3 seconds fuse time + tnt.yield = 5.5f + } + } + } + + override fun handleBlockPhysics(event: BlockPhysicsEvent) { + if (event.block.type == Material.OAK_PLANKS) { + event.isCancelled = true } } @@ -693,6 +861,11 @@ class HealthShopMinigame( override fun handleInventoryOpen(event: InventoryOpenEvent) { val player = event.player + if (player.gameMode == GameMode.SPECTATOR) { + event.isCancelled = true + return + } + val inventory = event.inventory val holder = inventory.holder if (holder is Chest) { @@ -729,21 +902,16 @@ class HealthShopMinigame( if (player.gameMode == GameMode.CREATIVE) { return } - val perk = - player.persistentDataContainer.get( - NamespacedKey(PartyGames.plugin, "double_jump"), - PersistentDataType.BOOLEAN, - ) - if (perk != true) { + if (!getPlayerData(player).doubleJump) { return } // check for last double jump val lastDoubleJumpTime = lastDoubleJump[player.uniqueId] - if (lastDoubleJumpTime != null && System.currentTimeMillis() - lastDoubleJumpTime < 3000) { + if (lastDoubleJumpTime != null && Bukkit.getCurrentTick() - lastDoubleJumpTime < 60) { event.isCancelled = true return } - lastDoubleJump[player.uniqueId] = System.currentTimeMillis() + lastDoubleJump[player.uniqueId] = Bukkit.getCurrentTick() event.isCancelled = true player.allowFlight = false player.isFlying = false @@ -765,12 +933,10 @@ class HealthShopMinigame( player.playSound(sound, Sound.Emitter.self()) } - override val name: Component - get() = Component.text("Health Shop", NamedTextColor.AQUA) - override val description: Component - get() = - Component.text( - "Buy items and weapons to fight in a free for all battleground.\nWatch out, the items cost not money, but your own health!", - NamedTextColor.AQUA, - ) + override val name = mm.deserialize("Health Shop v1.1") + override val description = + Component.text( + "Buy items and weapons to fight in a free for all battleground.\nWatch out, the items cost not money, but your own health!", + NamedTextColor.AQUA, + ) } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/MineguessrMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/MineguessrMinigame.kt new file mode 100644 index 0000000..7459c5b --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/MineguessrMinigame.kt @@ -0,0 +1,257 @@ +package info.mester.network.partygames.game + +import info.mester.network.partygames.PartyGames +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.Minigame +import io.papermc.paper.event.player.AsyncChatEvent +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import net.kyori.adventure.translation.GlobalTranslator +import org.bukkit.Bukkit +import org.bukkit.ChunkSnapshot +import org.bukkit.GameMode +import org.bukkit.GameRule +import org.bukkit.World +import org.bukkit.block.Biome +import org.bukkit.event.block.BlockPhysicsEvent +import java.util.Locale +import java.util.UUID +import java.util.concurrent.CompletableFuture +import kotlin.random.Random + +private fun levenshteinDistance( + a: String, + b: String, +): Int { + val dp = Array(a.length + 1) { IntArray(b.length + 1) } + // Initialize the base cases + for (i in 0..a.length) dp[i][0] = i + for (j in 0..b.length) dp[0][j] = j + // Fill the DP table + for (i in 1..a.length) { + for (j in 1..b.length) { + val cost = if (a[i - 1] == b[j - 1]) 0 else 1 + dp[i][j] = + minOf( + dp[i - 1][j] + 1, // Deletion + dp[i][j - 1] + 1, // Insertion + dp[i - 1][j - 1] + cost, // Substitution + ) + } + } + + return dp[a.length][b.length] +} + +private fun Biome.getBiomeName(locale: Locale): String = + PlainTextComponentSerializer + .plainText() + .serialize(GlobalTranslator.render(Component.translatable(translationKey()), locale)) + .split(" ") + .joinToString(" ") { word -> + word.replaceFirstChar { letter -> letter.uppercase() } + } + +enum class MineguessrState { + LOADING, + GUESSING, +} + +@Suppress("Unused") +class MineguessrMinigame( + game: Game, +) : Minigame(game, "mineguessr") { + companion object { + private var sourceWorld: World = Bukkit.getWorld("world")!! + private var maxSize = 1000 + private val disallowedBiomes = listOf(Biome.DRIPSTONE_CAVES) + + init { + reload() + } + + fun reload() { + val plugin = PartyGames.plugin + plugin.logger.info("Loading mineguessr config...") + val worldName = plugin.config.getString("mineguessr.world")!! + val maxSize = plugin.config.getInt("mineguessr.max-size") + sourceWorld = Bukkit.getWorld(worldName)!! + this.maxSize = maxSize + } + } + + private var remainingRounds = 10 + private var state = MineguessrState.LOADING + private val biomeList = mutableListOf() + private val guessed = mutableListOf() + + private fun getRandomChunkAsync(): CompletableFuture { + var worldSize = sourceWorld.worldBorder.size + worldSize -= sourceWorld.worldBorder.size % 16 + worldSize /= 32 // 16 blocks per chunk, and an extra 2 division to make it a radius + val worldSizeInt = worldSize.toInt().coerceAtMost(maxSize) + val chunkX = Random.nextInt(-worldSizeInt, worldSizeInt) + val chunkZ = Random.nextInt(-worldSizeInt, worldSizeInt) + val future = sourceWorld.getChunkAtAsync(chunkX, chunkZ, true) + return future.thenApply { chunk -> + chunk.getChunkSnapshot(true, true, false, false) + } + } + + private fun copyChunk(chunk: ChunkSnapshot) { + // we only want to copy the top 32 blocks into startPos' chunk + // first, find the highest block + var highestBlockY = -999 + for (x in 0..15) { + for (z in 0..15) { + val highestBlock = chunk.getHighestBlockYAt(x, z) + if (highestBlock > highestBlockY) { + highestBlockY = highestBlock + } + } + } + // now we can begin the copy + for (x in 0..15) { + for (z in 0..15) { + for (y in 0..32) { + val chunkY = (highestBlockY - 32 + y).coerceAtLeast(-64) + val chunkBlockData = chunk.getBlockData(x, chunkY, z) + val gameBlock = game.world.getBlockAt(x, y, z) + gameBlock.type = chunkBlockData.material + gameBlock.blockData = chunkBlockData.clone() + val chunkBiome = chunk.getBiome(x, chunkY, z) + game.world.setBiome(x, y, z, chunkBiome) + if (!biomeList.contains(chunkBiome) && !disallowedBiomes.contains(chunkBiome)) { + biomeList.add(chunkBiome) + } + } + } + } + // send a chunk update to everyone + game.world.refreshChunk(0, 0) + } + + private fun loadChunk(): CompletableFuture { + audience.sendActionBar(Component.text("Loading chunk...", NamedTextColor.YELLOW)) + biomeList.clear() + val future = getRandomChunkAsync() + return future.thenCompose { chunk -> + copyChunk(chunk) + audience.sendMessage( + MiniMessage.miniMessage().deserialize( + "${"-".repeat(30)}\n" + + "Loading finished, time to guess!\n" + + "This chunk contains ${biomeList.size} biomes.", + ), + ) + CompletableFuture.completedFuture(null) + } + } + + private fun startRound() { + state = MineguessrState.LOADING + loadChunk().thenRun { + guessed.clear() + state = MineguessrState.GUESSING + startCountdown(15 * 20) { + finishRound() + } + } + } + + private fun finishRound() { + stopCountdown() + // turn biomes into this: "Biome Name, "Biome Name2", "Biome Name3" + for (player in game.onlinePlayers) { + val biomeText = biomeList.joinToString(", ") { it.getBiomeName(player.locale()) } + player.sendMessage( + MiniMessage.miniMessage().deserialize("The chunk had these biomes: $biomeText"), + ) + } + guessed.clear() + // delay the new round by 1 tick, to give time for the end message to appear + Bukkit.getScheduler().runTaskLater( + PartyGames.plugin, + Runnable { + remainingRounds-- + if (remainingRounds > 0) { + startRound() + } else { + end() + } + }, + 1, + ) + } + + override fun finish() { + guessed.clear() + } + + override fun onLoad() { + game.world.setGameRule(GameRule.REDUCED_DEBUG_INFO, true) + super.onLoad() + } + + override fun start() { + super.start() + + for (player in game.onlinePlayers) { + player.gameMode = GameMode.SPECTATOR + } + + startRound() + } + + override fun handleBlockPhysics(event: BlockPhysicsEvent) { + event.isCancelled = true + } + + override fun handlePlayerChat(event: AsyncChatEvent) { + if (!running) { + return + } + if (guessed.contains(event.player.uniqueId)) { + event.player.sendMessage(Component.text("You already guessed!", NamedTextColor.RED)) + event.isCancelled = true + return + } + val plainText = PlainTextComponentSerializer.plainText().serialize(event.message()) + val biomes = + biomeList.map { + it.getBiomeName(event.player.locale()).uppercase() + } + if (plainText.uppercase() in biomes) { + audience.sendMessage( + MiniMessage + .miniMessage() + .deserialize(("${event.player.name} guessed the biome as #${guessed.size + 1}!")), + ) + val score = + when (guessed.size) { + 0 -> 5 + 1 -> 3 + 2 -> 2 + else -> 1 + } + game.addScore(event.player, score, "Correct guess") + guessed.add(event.player.uniqueId) + event.isCancelled = true + // check if everyone has guessed already + if (guessed.size >= onlinePlayers.size) { + finishRound() + } + return + } + val minDistance = biomes.minOfOrNull { levenshteinDistance(it, plainText.uppercase()) } ?: Int.MAX_VALUE + if (minDistance <= 3) { + event.player.sendMessage(Component.text("You are close!", NamedTextColor.YELLOW)) + } + } + + override val name = Component.text("Mineguessr", NamedTextColor.AQUA) + override val description = + Component.text("Guess the chunk based on a random segment of the world!", NamedTextColor.AQUA) +} diff --git a/src/main/kotlin/info/mester/network/partygames/game/Queue.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Queue.kt similarity index 96% rename from src/main/kotlin/info/mester/network/partygames/game/Queue.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Queue.kt index bee84e1..53b13c1 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/Queue.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Queue.kt @@ -1,7 +1,8 @@ package info.mester.network.partygames.game import info.mester.network.partygames.PartyGames -import info.mester.network.partygames.util.createBasicItem +import info.mester.network.partygames.api.MinigameBundle +import info.mester.network.partygames.api.createBasicItem import net.kyori.adventure.audience.Audience import net.kyori.adventure.key.Key import net.kyori.adventure.sound.Sound @@ -93,9 +94,9 @@ private class CountdownTask( } class Queue( - val type: GameType, + val bundle: MinigameBundle, val maxPlayers: Int, - private val manager: GameManager, + private val manager: QueueManager, ) { private val players = mutableListOf() private val countdownTask = CountdownTask(this) @@ -232,7 +233,7 @@ class Queue( event.setUseInteractedBlock(Event.Result.DENY) if (readyCooldown.containsKey(event.player.uniqueId)) { val diff = System.currentTimeMillis() - readyCooldown[event.player.uniqueId]!! - // 100 milliseconds is enough to fix a player instantly readying then leaving when they double click on the item + // 100 milliseconds is enough to fix a player instantly readying then leaving when they double-click on the item if (diff < 100) { return } @@ -256,7 +257,7 @@ class Queue( event.setUseInteractedBlock(Event.Result.DENY) if (readyCooldown.containsKey(event.player.uniqueId)) { val diff = System.currentTimeMillis() - readyCooldown[event.player.uniqueId]!! - // 100 milliseconds is enough to fix a player instantly readying then leaving when they double click on the item + // 100 milliseconds is enough to fix a player instantly readying then leaving when they double-click on the item if (diff < 100) { return } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/QueueManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/QueueManager.kt new file mode 100644 index 0000000..894eaf0 --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/QueueManager.kt @@ -0,0 +1,128 @@ +package info.mester.network.partygames.game + +import de.simonsator.partyandfriends.spigot.api.pafplayers.PAFPlayerManager +import de.simonsator.partyandfriends.spigot.api.party.PartyManager +import info.mester.network.partygames.PartyGames +import info.mester.network.partygames.api.MinigameBundle +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import java.util.UUID + +private val mm = MiniMessage.miniMessage() + +class QueueManager( + plugin: PartyGames, +) { + companion object { + val partyAvailable: Boolean + get() { + return try { + Class.forName("de.simonsator.partyandfriends.spigot.api.pafplayers.PAFPlayerManager") + true + } catch (_: ClassNotFoundException) { + false + } + } + + fun getPartyManager(): PartyManager? = + if (partyAvailable) { + PartyManager.getInstance() + } else { + null + } + + fun getPAFPlayerManager(): PAFPlayerManager? = + if (partyAvailable) { + PAFPlayerManager.getInstance() + } else { + null + } + } + + private val core = plugin.core + private val gameRegistry = core.gameRegistry + private val queues = mutableMapOf() + + private fun createQueue( + bundle: MinigameBundle, + maxPlayers: Int = 8, + ): Queue { + val queue = Queue(bundle, maxPlayers, this) + queues[queue.id] = queue + return queue + } + + private fun getQueueForPlayers( + bundle: MinigameBundle, + players: List, + ): Queue { + // either return the first queue that can still fit the players, or create a new queue + val queue = queues.values.firstOrNull { it.bundle == bundle && it.maxPlayers - it.playerCount >= players.size } + if (queue != null) { + return queue + } + return createQueue(bundle) + } + + fun removeQueue(id: UUID) { + queues.remove(id) + } + + fun joinQueue( + bundle: MinigameBundle, + player: Player, + ) { + if (partyAvailable) { + val pafPlayer = getPAFPlayerManager()!!.getPlayer(player.uniqueId) + val party = getPartyManager()!!.getParty(pafPlayer) + if (party != null) { + if (party.leader.uniqueId != player.uniqueId) { + Audience.audience(player).sendMessage( + mm.deserialize("You must be the party leader to join a game!"), + ) + return + } + val players = party.allPlayers.mapNotNull { Bukkit.getPlayer(it.uniqueId) } + joinQueue(bundle, players) + return + } + } + joinQueue(bundle, listOf(player)) + } + + private fun joinQueue( + bundle: MinigameBundle, + players: List, + ) { + // check if there is a player that is already in a game + val playersInGame = players.filter { gameRegistry.getGameOf(it) != null } + if (playersInGame.isNotEmpty()) { + Audience.audience(playersInGame).sendMessage( + mm.deserialize( + "You are already in a game! You cannot join another game!", + ), + ) + return + } + // remove players from queues that already have them + for (player in players) { + removePlayerFromQueue(player) + } + val queue = getQueueForPlayers(bundle, players) + queue.addPlayers(players) + } + + fun getQueueOf(player: Player) = queues.values.firstOrNull { it.hasPlayer(player) } + + fun removePlayerFromQueue(player: Player) { + getQueueOf(player)?.removePlayer(player) + } + + fun startGame(queue: Queue) { + queues.remove(queue.id) + val players = queue.getPlayers() + gameRegistry.startGame(players, queue.bundle.name) + } +} diff --git a/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt similarity index 93% rename from src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt index 6278234..2849d00 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt @@ -3,6 +3,8 @@ package info.mester.network.partygames.game import de.exlll.configlib.YamlConfigurationProperties import de.exlll.configlib.YamlConfigurationStore import info.mester.network.partygames.PartyGames.Companion.plugin +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.Minigame import info.mester.network.partygames.game.snifferhunt.RideableSniffer import info.mester.network.partygames.game.snifferhunt.SnifferHuntConfig import info.mester.network.partygames.game.snifferhunt.TreasureRarity @@ -158,12 +160,10 @@ class SnifferHuntMinigame( } } - override val name: Component - get() = Component.text("Sniffer Hunt", NamedTextColor.AQUA) - override val description: Component - get() = - Component.text( - "Use your sniffer to hunt for items.\nTrade the items and use them to craft weapons and upgrades for the final battle!", - NamedTextColor.AQUA, - ) + override val name = Component.text("Sniffer Hunt", NamedTextColor.AQUA) + override val description = + Component.text( + "Use your sniffer to hunt for items.\nTrade the items and use them to craft weapons and upgrades for the final battle!", + NamedTextColor.AQUA, + ) } diff --git a/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt similarity index 57% rename from src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt index 1f49f6b..8e8ee62 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt @@ -3,39 +3,56 @@ package info.mester.network.partygames.game import com.sk89q.worldedit.math.BlockVector3 import com.sk89q.worldedit.regions.CuboidRegion import info.mester.network.partygames.PartyGames +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.Minigame import info.mester.network.partygames.mm import info.mester.network.partygames.pow import info.mester.network.partygames.util.WeightedItem import info.mester.network.partygames.util.selectWeightedRandom +import info.mester.network.partygames.util.snapTo90 import io.papermc.paper.event.block.BlockBreakProgressUpdateEvent +import io.papermc.paper.event.player.PrePlayerAttackEntityEvent import net.kyori.adventure.text.Component +import net.kyori.adventure.text.JoinConfiguration import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.title.Title +import net.kyori.adventure.title.TitlePart +import net.kyori.adventure.translation.GlobalTranslator import org.bukkit.Bukkit +import org.bukkit.Difficulty import org.bukkit.GameMode import org.bukkit.Location import org.bukkit.Material +import org.bukkit.NamespacedKey import org.bukkit.World import org.bukkit.block.Banner import org.bukkit.block.BlockState +import org.bukkit.block.data.Bisected +import org.bukkit.block.data.BlockData import org.bukkit.block.data.type.ChiseledBookshelf import org.bukkit.block.data.type.Fence import org.bukkit.block.data.type.Gate import org.bukkit.block.data.type.GlassPane import org.bukkit.block.data.type.Slab +import org.bukkit.block.data.type.Stairs import org.bukkit.block.data.type.TrapDoor import org.bukkit.block.structure.Mirror import org.bukkit.block.structure.StructureRotation import org.bukkit.configuration.file.YamlConfiguration +import org.bukkit.entity.Entity import org.bukkit.entity.Player +import org.bukkit.event.Event import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.block.BlockPhysicsEvent import org.bukkit.event.block.BlockPlaceEvent +import org.bukkit.event.entity.CreatureSpawnEvent import org.bukkit.event.inventory.InventoryOpenEvent import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.event.player.PlayerMoveEvent import org.bukkit.inventory.ItemStack import org.bukkit.inventory.meta.BannerMeta +import org.bukkit.inventory.meta.SpawnEggMeta +import org.bukkit.persistence.PersistentDataType import org.bukkit.structure.Structure import java.io.File import java.time.Duration @@ -56,40 +73,54 @@ enum class SpeedBuildersState { MEMORISE, BUILD, JUDGE, + FINISHED, } // StructureData data class which holds the name, difficulty and file name of the structure -data class StructureData( +private data class StructureData( val name: String, val difficulty: StructureDifficulty, + val displayName: String, ) { val fileName = "${name.lowercase()}.nbt" } +private data class StructureAccuracy( + val accuracy: Double, + val incorrectBlocks: List>, +) + +/** + * The size of the player area in blocks. + * The player area is a cuboid region with the size of PLAYER_AREA_SIZE x PLAYER_AREA_SIZE x PLAYER_AREA_SIZE (not including floor). + */ const val PLAYER_AREA_SIZE = 7 +/** + * How much padding should be between the player areas. + */ +const val AREA_OFFSET = 5 + private fun Location.toBlockVector(): BlockVector3 = BlockVector3.at(blockX, blockY, blockZ) private fun Location.toPlayerArea(): CuboidRegion { val corner1 = clone() val corner2 = - clone().add(PLAYER_AREA_SIZE.toDouble(), PLAYER_AREA_SIZE.toDouble(), PLAYER_AREA_SIZE.toDouble()) + clone().add((PLAYER_AREA_SIZE - 1).toDouble(), PLAYER_AREA_SIZE.toDouble(), (PLAYER_AREA_SIZE - 1).toDouble()) return CuboidRegion(corner1.toBlockVector(), corner2.toBlockVector()) } private fun CuboidRegion.toLocation(world: World): Location = Location(world, pos1.x().toDouble(), pos1.y().toDouble(), pos1.z().toDouble()) -/** - * How much padding should be between the player areas. - */ -const val AREA_OFFSET = 5 - class SpeedBuildersMinigame( game: Game, ) : Minigame(game, "speedbuilders") { companion object { - val plugin = PartyGames.plugin + private val plugin = PartyGames.plugin private val structures = mutableListOf() + private val scores = mutableMapOf() + private val gotItems = mutableListOf() + val SPAWNED_ENTITY_KEY = NamespacedKey(plugin, "spawned") fun reload() { val config = YamlConfiguration.loadConfiguration(File(plugin.dataFolder, "speed-builders.yml")) @@ -100,15 +131,37 @@ class SpeedBuildersMinigame( try { val structureConfig = config.getConfigurationSection("structures.$key")!! val difficulty = structureConfig.getString("difficulty")!! - structures.add(StructureData(key, StructureDifficulty.valueOf(difficulty.uppercase()))) + val displayName = structureConfig.getString("display_name", "Unknown")!! + val structureData = + StructureData(key, StructureDifficulty.valueOf(difficulty.uppercase()), displayName) + val structurePath = "speedbuilders/${structureData.fileName}" + // step 1: check if the structure file is inside the jar + val structureInJar = plugin.getResource(structurePath) != null + if (structureInJar) { + plugin.saveResource(structurePath, true) + } else { + // step 2: check if the structure file is already the plugin's data folder + val structureInDataFolder = File(plugin.dataFolder, structurePath).exists() + if (!structureInDataFolder) { + throw IllegalStateException("Structure file $structurePath not found!") + } + } + structures.add(structureData) } catch (e: Exception) { plugin.logger.warning("Failed to load structure $key") plugin.logger.log(Level.WARNING, e.message, e) } } - // load the structure files - for (structureData in structures) { - plugin.saveResource("speedbuilders/${structureData.fileName}", true) + // load scores + config.getConfigurationSection("scores")?.getKeys(false)?.forEach { key -> + try { + val difficulty = StructureDifficulty.valueOf(key.uppercase()) + val score = config.getConfigurationSection("scores")!!.getInt(key) + scores[difficulty] = score + } catch (e: Exception) { + plugin.logger.warning("Failed to load score for $key") + plugin.logger.log(Level.WARNING, e.message, e) + } } } @@ -135,42 +188,51 @@ class SpeedBuildersMinigame( throw IllegalStateException("No structure data is selected!") } val structureData = currentStructureData!! - val structureFile = structureData.fileName - return structureManager.loadStructure(File(plugin.dataFolder, "speedbuilders/$structureFile")) + val structurePath = "speedbuilders/${structureData.fileName}" + return structureManager.loadStructure(File(originalPlugin.dataFolder, structurePath)) } private fun selectStructure(difficulty: StructureDifficulty?): StructureData { // select a random structure based on the difficulty // if difficulty is null, select a random structure val structureData = - difficulty?.let { structures.filter { it.difficulty == difficulty }.random() } ?: structures.random() + difficulty?.let { structures.filter { it.difficulty == difficulty }.randomOrNull() } ?: structures.random() return structureData } private fun calculateAccuracy( original: Structure, copy: Structure, - ): Double { + ): StructureAccuracy { // for original blocks, disregard the very bottom layer (the floor) val originalBlocks = original.palettes[0].blocks.filter { it.type != Material.AIR && it.location.y > 0 } val copyBlocks = copy.palettes[0].blocks.filter { it.type != Material.AIR && it.location.y > 0 } + val incorrectBlocks = mutableListOf>() var correctBlocks = 0 // go through every block in the original structure for (originalBlock in originalBlocks) { // get the block in the copy structure at the same location - val copyBlock = copyBlocks.firstOrNull { it.location == originalBlock.location } ?: continue - // perform checks to see if the block is the same - if (originalBlock.type != copyBlock.type) { + val copyBlock = copyBlocks.firstOrNull { it.location == originalBlock.location } + if (copyBlock == null) { + // if the block is not found, it is incorrect + incorrectBlocks.add(originalBlock.blockData to null) continue } val originalBlockData = originalBlock.blockData val copyBlockData = copyBlock.blockData + val incorrectBlock = originalBlockData to copyBlockData + incorrectBlocks.add(incorrectBlock) + // perform checks to see if the block is the same + if (originalBlock.type != copyBlock.type) { + continue + } // special case for mushroom blocks: the sides are too difficult to replicate, so instead just ignore and check only for type if (originalBlock.type == Material.RED_MUSHROOM_BLOCK || originalBlock.type == Material.BROWN_MUSHROOM_BLOCK || originalBlock.type == Material.MUSHROOM_STEM ) { correctBlocks++ + incorrectBlocks.remove(incorrectBlock) continue } // special case for trapdoors @@ -187,6 +249,7 @@ class SpeedBuildersMinigame( continue } correctBlocks++ + incorrectBlocks.remove(incorrectBlock) continue } // special case for gates @@ -205,11 +268,30 @@ class SpeedBuildersMinigame( continue } correctBlocks++ + incorrectBlocks.remove(incorrectBlock) continue } // blocks where only type should be checked (the data is too complicated) if (originalBlockData is Fence || originalBlockData is GlassPane) { correctBlocks++ + incorrectBlocks.remove(incorrectBlock) + continue + } + // special code for stairs + if (originalBlockData is Stairs && copyBlockData is Stairs) { + // check for half + if (originalBlockData.half != copyBlockData.half) { + continue + } + // check if shape is STRAIGHT and facing is the same + if (originalBlockData.shape == Stairs.Shape.STRAIGHT && + (originalBlockData.shape != copyBlockData.shape || originalBlockData.facing != copyBlockData.facing) + ) { + continue + } + // the rest is too damn difficult to check + correctBlocks++ + incorrectBlocks.remove(incorrectBlock) continue } // general block data check @@ -222,15 +304,43 @@ class SpeedBuildersMinigame( continue } } + correctBlocks++ + incorrectBlocks.remove(incorrectBlock) + } + val originalEntities = original.entities + val copyEntities = copy.entities + var correctEntities = 0 + // go through every entity in the copy structure + for (copyEntity in copyEntities) { + val originalEntity = + originalEntities.firstOrNull { originalEntity -> + if (originalEntity.type != copyEntity.type) { + return@firstOrNull false + } + val originalLocation = originalEntity.location + val copyLocation = copyEntity.location + originalLocation.blockX == copyLocation.blockX && + originalLocation.blockY == copyLocation.blockY && + originalLocation.blockZ == copyLocation.blockZ + } + if (originalEntity == null) { + continue + } + // check if the rotation is the same + if (originalEntity.location.yaw != copyEntity.location.yaw) { + continue + } + correctEntities++ } - return correctBlocks.toDouble() / originalBlocks.size + val accuracy = (correctBlocks + correctEntities).toDouble() / (originalBlocks.size + originalEntities.size) + return StructureAccuracy(accuracy, incorrectBlocks) } private fun calculateAccuracy( original: Structure, location: Location, - ): Double { + ): StructureAccuracy { // create a strcture based on the play area val endPos = location @@ -242,6 +352,31 @@ class SpeedBuildersMinigame( return calculateAccuracy(original, copy) } + private fun giveItemFromEntity( + entity: Entity, + player: Player, + ) { + val entityType = entity.type + if (!entityType.isSpawnable || !entityType.isAlive) { + return + } + // attempt to get the spawn egg material + val spawnEggMaterialName = "${entityType.name}_SPAWN_EGG" + val spawnEggMaterial = Material.matchMaterial(spawnEggMaterialName) + if (spawnEggMaterial != null) { + player.inventory.addItem(ItemStack.of(spawnEggMaterial)) + } else { + // just create a polar spawn egg that spawns the entity + val spawnEgg = ItemStack.of(Material.POLAR_BEAR_SPAWN_EGG) + spawnEgg.editMeta { meta -> + meta as SpawnEggMeta + meta.displayName(Component.text("${entityType.name} Spawn Egg")) + meta.customSpawnedType = entityType + } + player.inventory.addItem(spawnEgg) + } + } + private fun giveItemsFromStructure( structure: Structure, player: Player, @@ -250,6 +385,10 @@ class SpeedBuildersMinigame( blocks.forEach { block -> giveItemFromBlock(block, player) } + val entities = structure.entities + entities.forEach { entity -> + giveItemFromEntity(entity, player) + } } private fun clearPlayerArea( @@ -259,6 +398,7 @@ class SpeedBuildersMinigame( val clearRegion = when (withFloor) { true -> playerArea + false -> { // offset the player area by 1 block val pos1 = playerArea.pos1.add(0, 1, 0) @@ -269,6 +409,15 @@ class SpeedBuildersMinigame( val location = Location(startPos.world, vec.x().toDouble(), vec.y().toDouble(), vec.z().toDouble()) location.block.type = Material.AIR } + for (entity in startPos.world.entities.filter { entity -> + playerArea.contains(entity.location.toBlockVector()) && + entity.persistentDataContainer.has( + SPAWNED_ENTITY_KEY, + PersistentDataType.BOOLEAN, + ) + }) { + entity.remove() + } } private fun eliminatePlayer(playerUUID: UUID) { @@ -288,6 +437,7 @@ class SpeedBuildersMinigame( private fun giveItemFromBlock( blockState: BlockState, player: Player, + ignoreBisected: Boolean = false, ) { // special code for fire if (blockState.type == Material.FIRE) { @@ -311,10 +461,20 @@ class SpeedBuildersMinigame( } } // special code for chiseled bookshelves - if (blockData is ChiseledBookshelf) { + if (blockData is ChiseledBookshelf && blockData.occupiedSlots.size > 0) { val books = ItemStack.of(Material.BOOK, blockData.occupiedSlots.size) player.inventory.addItem(books) } + // special code for double blocks (door, tall grass, tall flowers etc. make sure to ignore stairs and trapdoors) + if (!ignoreBisected && + blockData is Bisected && + blockData.half == Bisected.Half.TOP && + blockData !is Stairs && + blockData !is TrapDoor + ) { + // by returning, we only give an item for the bottom half of the block, instead of giving an item twice + return + } player.inventory.addItem(item) } @@ -322,12 +482,52 @@ class SpeedBuildersMinigame( player: Player, didLeave: Boolean, ) { - if (game.onlinePlayers.filter { it.gameMode == GameMode.SURVIVAL }.size <= 1) { - win() + if (didLeave) { + eliminatePlayer(player.uniqueId) + if (game.onlinePlayers.filter { it.gameMode == GameMode.SURVIVAL }.size <= 1) { + win() + } } } - fun handleBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) { + override fun handleRejoin(player: Player) { + player.allowFlight = true + player.isFlying = true + + when (state) { + SpeedBuildersState.MEMORISE -> { + player.gameMode = GameMode.SURVIVAL + player.showBossBar(game.remainingBossBar) + val playerArea = playerAreas[player.uniqueId]!! + val location = playerArea.toLocation(startPos.world) + player.teleport(location.clone().add(-0.5, 1.0, -0.5)) + } + + SpeedBuildersState.BUILD -> { + player.gameMode = GameMode.SURVIVAL + player.showBossBar(game.remainingBossBar) + + if (!gotItems.contains(player.uniqueId)) { + // give the player the items from the structure + player.inventory.clear() + giveItemsFromStructure(currentStructure!!, player) + gotItems.add(player.uniqueId) + } + } + + SpeedBuildersState.JUDGE -> { + player.gameMode = GameMode.SPECTATOR + } + + SpeedBuildersState.FINISHED -> { + player.gameMode = GameMode.SPECTATOR + } + } + + super.handleRejoin(player) + } + + override fun handleBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) { if (event.entity !is Player) return if (state != SpeedBuildersState.BUILD) return val player = event.entity as Player @@ -348,7 +548,7 @@ class SpeedBuildersMinigame( } blockBreakCooldowns[event.entity.uniqueId] = System.currentTimeMillis() // give the player the item from the block - giveItemFromBlock(event.block.state, player) + giveItemFromBlock(event.block.state, player, true) // break the block without dropping it event.block.type = Material.AIR } @@ -360,14 +560,16 @@ class SpeedBuildersMinigame( val player = event.player val playerArea = playerAreas[player.uniqueId] ?: return val blockLocation = event.block.location - if (blockLocation.y.toInt() == playerArea.pos1.y()) { + if (blockLocation.blockY == playerArea.pos1.y()) { // the player is trying to break the floor return } if (!playerArea.contains(blockLocation.toBlockVector())) { return } + blockBreakCooldowns[event.player.uniqueId] = System.currentTimeMillis() giveItemFromBlock(event.block.state, player) + event.block.type = Material.AIR } private fun checkForPerfect(player: Player) { @@ -376,8 +578,8 @@ class SpeedBuildersMinigame( return } val playerArea = playerAreas[player.uniqueId]!! - val accuracy = calculateAccuracy(structure, playerArea.toLocation(startPos.world)) - if (accuracy == 1.0) { + val accuracyResult = calculateAccuracy(structure, playerArea.toLocation(startPos.world)) + if (accuracyResult.accuracy == 1.0) { player.gameMode = GameMode.SPECTATOR audience.sendMessage(mm.deserialize("${player.name} has a perfect build!")) if (onlinePlayers.none { it.gameMode == GameMode.SURVIVAL }) { @@ -395,8 +597,7 @@ class SpeedBuildersMinigame( // check if the block's coordinates are in the player area val playerArea = playerAreas[player.uniqueId] ?: return val blockPos = event.block.location - val blockVector = BlockVector3.at(blockPos.x, blockPos.y, blockPos.z) - if (!playerArea.contains(blockVector)) { + if (!playerArea.contains(blockPos.toBlockVector())) { event.isCancelled = true player.sendMessage(Component.text("You can only place blocks in your play area!", NamedTextColor.RED)) return @@ -414,6 +615,11 @@ class SpeedBuildersMinigame( if (event.block.type == Material.AIR) { return } + // ignore floor blocks + if (event.block.location.blockY == startPos.blockY) { + event.isCancelled = true + return + } // check if the block is still supported if (event.block.blockData.isSupported(event.block.location)) { return @@ -457,9 +663,22 @@ class SpeedBuildersMinigame( override fun handlePlayerInteract(event: PlayerInteractEvent) { // no interactions during the memorise phase if (state == SpeedBuildersState.MEMORISE) { - event.isCancelled = true + event.setUseInteractedBlock(Event.Result.DENY) + event.setUseItemInHand(Event.Result.DENY) } if (state == SpeedBuildersState.BUILD) { + // check if the player tried to place a spawn egg and cancel the event if it was in a wrong position + kotlin.run { + val item = event.item ?: return@run + if (item.itemMeta !is SpawnEggMeta) return@run + val block = event.clickedBlock ?: return@run + val finalBlock = block.getRelative(event.blockFace) + val playerArea = playerAreas[event.player.uniqueId] ?: return@run + if (!playerArea.contains(finalBlock.location.toBlockVector())) { + event.setUseInteractedBlock(Event.Result.DENY) + event.setUseItemInHand(Event.Result.DENY) + } + } Bukkit.getScheduler().runTaskLater( plugin, Runnable { @@ -473,10 +692,43 @@ class SpeedBuildersMinigame( } } + override fun handleCreatureSpawn( + event: CreatureSpawnEvent, + player: Player, + ) { + val snappedAngle = snapTo90(player.location.yaw) + val spawnee = event.entity + spawnee.setRotation(snappedAngle, 0.0f) + spawnee.setAI(false) + spawnee.isSilent = true + spawnee.persistentDataContainer.set( + SPAWNED_ENTITY_KEY, + PersistentDataType.BOOLEAN, + true, + ) + } + + override fun handlePrePlayerAttack(event: PrePlayerAttackEntityEvent) { + val player = event.player + val target = event.attacked + if (state != SpeedBuildersState.BUILD) { + return + } + if (!target.persistentDataContainer.has(SPAWNED_ENTITY_KEY, PersistentDataType.BOOLEAN)) { + return + } + val playerArea = playerAreas[player.uniqueId] ?: return + if (!playerArea.contains(target.location.toBlockVector())) { + return + } + target.remove() + giveItemFromEntity(target, player) + } + private fun startMemorise() { state = SpeedBuildersState.MEMORISE // make sure that every player who became a spectator the last game due to perfect build is in survival again - for (player in game.onlinePlayers.filter { it.gameMode == GameMode.SPECTATOR && playerAreas.containsKey(it.uniqueId) }) { + for (player in game.onlinePlayers.filter { playerAreas.containsKey(it.uniqueId) }) { player.gameMode = GameMode.SURVIVAL player.allowFlight = true player.isFlying = true @@ -498,9 +750,16 @@ class SpeedBuildersMinigame( currentStructureData = selectStructure(difficulty) val structure = getStructure() currentStructure = structure + audience.sendTitlePart( + TitlePart.TIMES, + Title.Times.times(Duration.ofSeconds(0), Duration.ofSeconds(2), Duration.ofSeconds(0)), + ) + audience.sendTitlePart(TitlePart.TITLE, mm.deserialize("${currentStructureData!!.displayName}")) + audience.sendMessage(Component.text("Memorise the structure!", NamedTextColor.GREEN)) + // set up player areas for ((playerUUID, playerArea) in playerAreas) { + clearPlayerArea(playerArea, false) val player = Bukkit.getPlayer(playerUUID) ?: continue - player.sendMessage(Component.text("Memorise the structure!", NamedTextColor.GREEN)) // place down the structure in the play area val location = playerArea.toLocation(startPos.world) structure.place( @@ -514,31 +773,57 @@ class SpeedBuildersMinigame( ) // teleport the player to the platform player.teleport(location.clone().add(-0.5, 1.0, -0.5)) - player.isFlying = true + player.inventory.clear() } - startCountdown(10000) { + startCountdown(10 * 20) { startBuild() } } private fun startBuild() { state = SpeedBuildersState.BUILD + gotItems.clear() + val structure = currentStructure!! for ((playerUUID, playerArea) in playerAreas) { - val player = Bukkit.getPlayer(playerUUID) ?: continue // clear the player area clearPlayerArea(playerArea, false) // give items to the player + val player = Bukkit.getPlayer(playerUUID) ?: continue player.inventory.clear() - giveItemsFromStructure(currentStructure!!, player) + giveItemsFromStructure(structure, player) + gotItems.add(playerUUID) + } + // remove every entity spawned by the structure + for (entity in game.world.entities.filter { + it.persistentDataContainer.has( + NamespacedKey( + plugin, + "spawned", + ), + PersistentDataType.BOOLEAN, + ) + }) { + entity.remove() } audience.sendMessage(Component.text("Build the structure!", NamedTextColor.GREEN)) - startCountdown(30000) { + // the duration of the build phase is 30 seconds + 1 second per 5 blocks in the structure + // to get the block count, just check the player's inventory and count how many items it has in total + val blocksInStructure = + game.onlinePlayers + .first() + .inventory.contents + .filterNotNull() + .sumOf { it.amount } + val buildDuration = (30 + blocksInStructure / 5) * 20 + startCountdown(buildDuration) { startJudge() } } private fun startJudge() { + // we need to stop the countdown in case we got here due to everyone reaching a perfect build stopCountdown() + state = SpeedBuildersState.JUDGE val structure = currentStructure!! val accuracies = @@ -546,17 +831,15 @@ class SpeedBuildersMinigame( val location = playerArea.toLocation(startPos.world) player to calculateAccuracy(structure, location) } - val baseScore = - when (currentStructureData!!.difficulty) { - StructureDifficulty.EASY -> 10 - StructureDifficulty.MEDIUM -> 25 - StructureDifficulty.HARD -> 50 - StructureDifficulty.INSANE -> 85 - } + val baseScore = scores.getOrDefault(currentStructureData!!.difficulty, 0) + + for (player in game.onlinePlayers) { + player.gameMode = GameMode.SPECTATOR + } // show the accuracy of the player's structure and add score - for ((playerUUID, accuracy) in accuracies) { + for ((playerUUID, structureAccuracy) in accuracies) { val player = Bukkit.getPlayer(playerUUID) ?: continue - val accuracyString = String.format("%.2f", accuracy * 100) + val accuracyString = String.format("%.2f", structureAccuracy.accuracy * 100) player.showTitle( Title.title( Component.text("Accuracy: $accuracyString%", NamedTextColor.GREEN), @@ -564,14 +847,36 @@ class SpeedBuildersMinigame( Title.Times.times(Duration.ofSeconds(0), Duration.ofSeconds(5), Duration.ofSeconds(0)), ), ) - if (accuracy == 1.0) { + if (structureAccuracy.accuracy == 1.0) { game.addScore(player, (baseScore * 2.5).toInt(), "Perfect build") } else { - game.addScore(player, (accuracy * baseScore).toInt(), "$accuracyString% accuracy") + game.addScore(player, (structureAccuracy.accuracy * baseScore).toInt(), "$accuracyString% accuracy") + val incorrectMessage = + Component.text("Incorrect blocks: ", NamedTextColor.GRAY).append( + Component.join( + JoinConfiguration.commas(true), + structureAccuracy.incorrectBlocks.map { (original, copy) -> + val hoverText = + buildString { + append("Expected: $original, got: ${copy?.toString() ?: "nothing"}}") + } + val copyType = copy?.material ?: Material.AIR + GlobalTranslator.render( + Component + .translatable( + copyType.translationKey(), + NamedTextColor.RED, + ).hoverEvent(Component.text(hoverText)), + player.locale(), + ) + }, + ), + ) + player.sendMessage(incorrectMessage) } } // start a 5-second countdown and eliminate the worst players - startCountdown(5000, false) { + startCountdown(5 * 20, false) { // we may only eliminate max 1/5th of the playing players // a player is considered playing if they are in the playerAreas map val alivePlayers = game.onlinePlayers.filter { player -> playerAreas.containsKey(player.uniqueId) } @@ -580,8 +885,8 @@ class SpeedBuildersMinigame( // elements of the ascending sorted list of accuracies (excluding perfect matches) val worstPlayers = accuracies.entries - .filter { (_, accuracy) -> accuracy < 1.0 } - .sortedBy { (_, accuracy) -> accuracy } + .filter { (_, accuracy) -> accuracy.accuracy < 1.0 } + .sortedBy { (_, accuracy) -> accuracy.accuracy } .take(playersToEliminate) .map { (player, _) -> player } if (worstPlayers.isEmpty()) { @@ -589,15 +894,12 @@ class SpeedBuildersMinigame( } else { worstPlayers.forEach { player -> eliminatePlayer(player) } } - if (alivePlayers.size - worstPlayers.size <= 1) { + // look for win condition (only one player remaining) + if (alivePlayers.size - worstPlayers.size <= 1 && System.getProperty("partygames.dev", "false") != "true") { win() } else { // start a 3-second countdown to start the next round - startCountdown(3000, false) { - // clear every player area - for (playerArea in playerAreas.values) { - clearPlayerArea(playerArea, false) - } + startCountdown(3 * 20, false) { startMemorise() } } @@ -605,11 +907,20 @@ class SpeedBuildersMinigame( } private fun win() { + state = SpeedBuildersState.FINISHED val winner = game.onlinePlayers.firstOrNull { player -> playerAreas.containsKey(player.uniqueId) } audience.sendMessage(Component.text("The winner is ${winner?.name ?: "Nobody"}!", NamedTextColor.GREEN)) + if (winner != null) { + game.addScore(winner, 25, "You won!") + } end() } + override fun onLoad() { + game.world.difficulty = Difficulty.NORMAL + super.onLoad() + } + override fun start() { super.start() // set up the player area for every player @@ -627,13 +938,11 @@ class SpeedBuildersMinigame( startMemorise() } - override val name: Component - get() = Component.text("Speed builders", NamedTextColor.AQUA) - override val description: Component - get() = - Component.text( - "You will be given a random structure you have to memorise in 10 seconds.\n" + - "After the time runs out, you will have 30 seconds to replicate the structure.", - NamedTextColor.AQUA, - ) + override val name = Component.text("Speed builders", NamedTextColor.AQUA) + override val description = + Component.text( + "You will be given a random structure you have to memorise in 10 seconds.\n" + + "After the time runs out, you will have a little time to replicate the structure.", + NamedTextColor.AQUA, + ) } diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/Cactus.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Cactus.kt similarity index 94% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/Cactus.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Cactus.kt index dad2d15..4452f79 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/Cactus.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Cactus.kt @@ -1,6 +1,6 @@ package info.mester.network.partygames.game.gardening -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import org.bukkit.Location import org.bukkit.Material import org.bukkit.util.Vector diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/GardenTap.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/GardenTap.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/GardenTap.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/GardenTap.kt diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/Lilac.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Lilac.kt similarity index 95% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/Lilac.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Lilac.kt index 994b471..1bfb955 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/Lilac.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Lilac.kt @@ -1,6 +1,6 @@ package info.mester.network.partygames.game.gardening -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import org.bukkit.Location import org.bukkit.Material import org.bukkit.block.data.Bisected diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/OakTree.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/OakTree.kt similarity index 98% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/OakTree.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/OakTree.kt index 7494054..0b255bc 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/OakTree.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/OakTree.kt @@ -1,6 +1,6 @@ package info.mester.network.partygames.game.gardening -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import org.bukkit.Location import org.bukkit.Material import org.bukkit.TreeType diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/Peony.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Peony.kt similarity index 95% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/Peony.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Peony.kt index b773494..f4f7f59 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/Peony.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Peony.kt @@ -1,6 +1,6 @@ package info.mester.network.partygames.game.gardening -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import org.bukkit.Location import org.bukkit.Material import org.bukkit.block.data.Bisected diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/Plant.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Plant.kt similarity index 96% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/Plant.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Plant.kt index dab910f..b95da1a 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/Plant.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Plant.kt @@ -1,6 +1,6 @@ package info.mester.network.partygames.game.gardening -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import info.mester.network.partygames.roundTo import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.TextColor @@ -122,8 +122,8 @@ abstract class Plant( fun killWeed(player: Player): Boolean { val score = getWeedKillScore() if (score == 0) { - // punish the player with -10 points for killing a non-weed - game.addScore(player, -10, "Killed non-weed") + // punish the player for killing a non-weed + game.addScore(player, -5, "Killed non-weed") return false } // give points to player diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/RainbowFlower.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/RainbowFlower.kt similarity index 95% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/RainbowFlower.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/RainbowFlower.kt index 80a0984..fe165ca 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/RainbowFlower.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/RainbowFlower.kt @@ -1,6 +1,6 @@ package info.mester.network.partygames.game.gardening -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import info.mester.network.partygames.pow import org.bukkit.Location import org.bukkit.Material diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/Rose.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Rose.kt similarity index 95% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/Rose.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Rose.kt index 13ff373..3449c5c 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/Rose.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Rose.kt @@ -1,6 +1,6 @@ package info.mester.network.partygames.game.gardening -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import org.bukkit.Location import org.bukkit.Material import org.bukkit.block.data.Bisected diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/Sunflower.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Sunflower.kt similarity index 95% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/Sunflower.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Sunflower.kt index 347aff3..0fdc2c5 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/Sunflower.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Sunflower.kt @@ -1,6 +1,6 @@ package info.mester.network.partygames.game.gardening -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import org.bukkit.Location import org.bukkit.Material import org.bukkit.block.data.Bisected diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/Weed.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Weed.kt similarity index 92% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/Weed.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Weed.kt index cf1ad02..90eb4c8 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/Weed.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/Weed.kt @@ -1,6 +1,6 @@ package info.mester.network.partygames.game.gardening -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import org.bukkit.Location import org.bukkit.Material import org.bukkit.util.Vector diff --git a/src/main/kotlin/info/mester/network/partygames/game/gardening/ZombieWeed.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/ZombieWeed.kt similarity index 75% rename from src/main/kotlin/info/mester/network/partygames/game/gardening/ZombieWeed.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/ZombieWeed.kt index b442320..a2f1b9b 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/gardening/ZombieWeed.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/gardening/ZombieWeed.kt @@ -1,7 +1,7 @@ package info.mester.network.partygames.game.gardening import info.mester.network.partygames.PartyGames -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import org.bukkit.Bukkit import org.bukkit.Location import org.bukkit.Material @@ -28,14 +28,16 @@ class ZombieWeed( private fun spawnEnemy() { // spawn an invisible zombie that'll act as the weed enemy - location.world.spawn(location.clone().add(0.5, 0.0, 0.5), Zombie::class.java) { entity -> - entity.isInvisible = true - entity.isCollidable = false - entity.isSilent = true - entity.isInvulnerable = true - entity.addPotionEffect(PotionEffect(PotionEffectType.INVISIBILITY, -1, 255, false, false, false)) - entity.getAttribute(Attribute.FOLLOW_RANGE)?.baseValue = 64.0 - entity.getAttribute(Attribute.ATTACK_DAMAGE)?.baseValue = 2.0 + location.world.spawn(location.clone().add(0.5, 0.0, 0.5), Zombie::class.java) { zombie -> + zombie.isInvisible = true + zombie.isCollidable = false + zombie.isSilent = true + zombie.isInvulnerable = true + zombie.addPotionEffect(PotionEffect(PotionEffectType.INVISIBILITY, -1, 255, false, false, false)) + zombie.getAttribute(Attribute.FOLLOW_RANGE)?.baseValue = 64.0 + zombie.getAttribute(Attribute.ATTACK_DAMAGE)?.baseValue = 2.0 + zombie.setShouldBurnInDay(false) + zombie.target = location.world.getNearbyPlayers(zombie.location, 16.0).firstOrNull() // spawn a block display to act as the weed location.world.spawn(location, BlockDisplay::class.java) { blockDisplay -> blockDisplay.block = Material.DEAD_BUSH.createBlockData() @@ -52,12 +54,12 @@ class ZombieWeed( Bukkit .getScheduler() .runTaskTimer(PartyGames.plugin, { t -> - if (entity.isDead) { + if (!zombie.isValid) { t.cancel() blockDisplay.remove() return@runTaskTimer } - blockDisplay.teleport(entity.location) + blockDisplay.teleport(zombie.location) }, 0, 1) } } diff --git a/src/main/kotlin/info/mester/network/partygames/game/healthshop/ArmorType.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/ArmorType.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/healthshop/ArmorType.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/ArmorType.kt diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopItem.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopItem.kt new file mode 100644 index 0000000..4ed3d1b --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopItem.kt @@ -0,0 +1,130 @@ +package info.mester.network.partygames.game.healthshop + +import info.mester.network.partygames.api.createBasicItem +import io.papermc.paper.datacomponent.DataComponentTypes +import org.bukkit.Material +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.inventory.ItemFlag +import org.bukkit.inventory.ItemStack + +enum class HealthShopItemCategory( + val displayItem: Material, +) { + /** + * Combat items, such as weapons and armor. + */ + COMBAT(Material.DIAMOND_SWORD), + + /** + * Utility items, such as potions and food. + */ + UTILITY(Material.GOLDEN_APPLE), + + /** + * Potions. + */ + POTION(Material.POTION), + + /** + * Miscellaneous items that do not fit into other categories. + */ + MISCELLANEOUS(Material.COMPASS), +} + +class HealthShopItem( + val item: ItemStack, + val price: Int, + val slot: Int, + val key: String, + val group: String, + val amount: Int = 1, + val category: HealthShopItemCategory = HealthShopItemCategory.MISCELLANEOUS, +) { + companion object { + @Suppress("UnstableApiUsage") + fun loadFromConfig( + section: ConfigurationSection, + key: String, + ): HealthShopItem { + val material = Material.matchMaterial(section.getString("id")!!) ?: Material.BARRIER + val amount = section.getInt("amount", 1) + val group = section.getString("group") ?: "none" + val lore = + ( + section.getStringList("lore") + + listOf( + "", + "Cost: ${String.format("%.1f", section.getInt("price") / 2.0)} ♥", + ) + ).toTypedArray() + val item = + createBasicItem( + material, + section.getString("name") ?: "Unknown", + amount, + *lore, + ).apply { + addItemFlags(ItemFlag.HIDE_ADDITIONAL_TOOLTIP) + val defaultMaxStack = material.maxStackSize + if (defaultMaxStack < amount) { + setData(DataComponentTypes.MAX_STACK_SIZE, amount) + this.amount = amount + } + } + item.editMeta { meta -> + meta.setEnchantmentGlintOverride(false) + HealthShopUI.applyGenericItemMeta(meta) + } + // apply healing potion to item + if (group == "splash_healing" || group == "splash_healing_ii") { + HealthShopUI.setHealthPotion(item, key == "splash_healing_ii") + } + // apply regeneration potion to item + if (group == "regen_ii") { + HealthShopUI.setRegen2Potion(item) + } + if (key == "regen_v") { + HealthShopUI.setRegenPotion(item, false) + } + // apply speed potion to item + if (group == "speed_ii") { + HealthShopUI.setSpeedPotion(item, false) + } + // apply jump potion to item + if (group == "jump_boost") { + HealthShopUI.setJumpPotion(item, false) + } + // apply turtle master + if (group == "turtle_master") { + val long = key.endsWith("_long") + val strong = key.endsWith("_strong") + HealthShopUI.setTurtleMasterPotion(item, long, strong, false) + } + // apply poison + if (group == "poison") { + HealthShopUI.setPoisonPotion(item, if (key == "poison_ii") 1 else 0, false) + } + // apply blindness potion + if (group == "blindness") { + HealthShopUI.setBlindnessPotion(item, false) + } + // apply levitation potion + if (group == "levitation") { + HealthShopUI.setLevitationPotion(item, if (key == "levitation_ii") 1 else 0, false) + } + + return HealthShopItem( + item, + section.getInt("price"), + section.getInt("slot"), + key, + group, + amount, + category = + HealthShopItemCategory.entries.firstOrNull { + it.name == section.getString("category")?.uppercase() + } ?: HealthShopItemCategory.MISCELLANEOUS, + ) + } + } +} diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopKit.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopKit.kt new file mode 100644 index 0000000..10119fc --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopKit.kt @@ -0,0 +1,52 @@ +package info.mester.network.partygames.game.healthshop + +import info.mester.network.partygames.mm +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.bukkit.inventory.ItemStack + +data class HealthShopKit( + val items: List, + val index: Int, +) { + fun getDisplayItem(): ItemStack = + items.maxByOrNull { it.price }?.item?.clone()?.apply { + editMeta { meta -> + val name = if (index == 8) "Last used kit" else "Saved kit #${index + 1}" + meta.displayName(mm.deserialize(name)) + + val lore = mutableListOf() + val items = items.sortedByDescending { it.price } + for (item in items) { + lore.add( + mm.deserialize( + " - ${ + String.format( + "%.1f", + item.price / 2.0, + ) + } ♥", + Placeholder.component("name", (item.item.itemMeta.displayName() ?: Component.empty())), + ), + ) + } + lore.add( + mm.deserialize( + "Total: ${ + String.format( + "%.1f", + items.sumOf { it.price } / 2.0, + ) + } ♥", + ), + ) + + lore.add(Component.empty()) + lore.add(mm.deserialize("Click to select this kit!")) + if (index != 8) { + lore.add(mm.deserialize("Right click to delete this kit!")) + } + meta.lore(lore) + } + } ?: ItemStack.empty() +} diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopPlayerData.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopPlayerData.kt new file mode 100644 index 0000000..d86219a --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopPlayerData.kt @@ -0,0 +1,10 @@ +package info.mester.network.partygames.game.healthshop + +data class HealthShopPlayerData( + var maxArrows: Int = 0, + var stealPerk: Boolean = false, + var healPerk: Boolean = false, + var doubleJump: Boolean = false, + var featherFall: Boolean = false, + var blastProtection: Boolean = false, +) diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopUI.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopUI.kt new file mode 100644 index 0000000..8f1d114 --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopUI.kt @@ -0,0 +1,848 @@ +package info.mester.network.partygames.game.healthshop + +import info.mester.network.partygames.PartyGames +import info.mester.network.partygames.api.createBasicItem +import info.mester.network.partygames.game.HealthShopMinigame +import info.mester.network.partygames.game.ShopFailedException +import info.mester.network.partygames.mm +import info.mester.network.partygames.toRomanNumeral +import io.papermc.paper.datacomponent.DataComponentTypes +import io.papermc.paper.datacomponent.item.UseCooldown +import net.kyori.adventure.key.Key +import net.kyori.adventure.sound.Sound +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.Style +import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.translation.GlobalTranslator +import org.bukkit.Bukkit +import org.bukkit.Color +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.InventoryHolder +import org.bukkit.inventory.ItemFlag +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.ItemMeta +import org.bukkit.inventory.meta.PotionMeta +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import org.bukkit.potion.PotionType +import java.util.Locale +import java.util.UUID + +class HealthShopUI( + private val playerUUID: UUID, + private var money: Double, + private var category: HealthShopItemCategory = HealthShopItemCategory.COMBAT, +) : InventoryHolder { + private val inventory = Bukkit.createInventory(this, 5 * 9, Component.text("Health Shop")) + private val purchasedItems: MutableList = mutableListOf() + private val databaseManager = PartyGames.plugin.databaseManager + private val kits = databaseManager.getHealthShopKits(playerUUID).toMutableList() + + /** + * Get the player + */ + private val player get() = Bukkit.getPlayer(playerUUID)!! + val playerData = HealthShopPlayerData() + + companion object { + private val shopItems get() = HealthShopMinigame.getShopItems() + private val INF_GAP_COOLDOWN_KEY = NamespacedKey(PartyGames.plugin, "inf_gap_cooldown") + + private fun setCustomPotion( + item: ItemStack, + potionEffects: List, + color: Color, + potionName: String?, + showExtraData: Boolean = true, + ) { + item.editMeta(PotionMeta::class.java) { meta -> + applyGenericItemMeta(meta) + + potionEffects.forEach { potionEffect -> + meta.addCustomEffect(potionEffect, true) + } + meta.color = color + + val duration = potionEffects.minBy { it.duration }.duration / 20 + val minutes = duration / 60 + val seconds = String.format("%02d", duration % 60) + + if (potionName != null) { + val name = + MiniMessage + .miniMessage() + .deserialize( + buildString { + append("$potionName ") + if (showExtraData) { + append( + "${(potionEffects[0].amplifier + 1).toRomanNumeral()} ($minutes:$seconds)", + ) + } + }, + ) + meta.displayName(name) + } + + // if we have a composite potion, display all the effects + if (potionEffects.size > 1) { + val lore = mutableListOf() + for (effect in potionEffects) { + val name = + GlobalTranslator.render( + Component.translatable( + effect.type.translationKey(), + Style + .style( + NamedTextColor.BLUE, + ).decoration(TextDecoration.ITALIC, false), + ), + Locale.US, + ) + val duration = effect.duration / 20 + val minutes = duration / 60 + val seconds = String.format("%02d", duration % 60) + + val durationData = + mm.deserialize( + " ${(effect.amplifier + 1).toRomanNumeral()} ($minutes:$seconds)", + ) + lore.add(name.append(durationData)) + } + val currentLore = meta.lore() ?: mutableListOf() + if (currentLore.isNotEmpty()) { + lore.add(Component.empty()) + } + lore.addAll(currentLore) + meta.lore(lore) + } + } + } + + private fun setCustomPotion( + item: ItemStack, + potionEffect: PotionEffect, + color: Color, + potionName: String?, + ) = setCustomPotion( + item, + listOf(potionEffect), + color, + potionName, + ) + + fun setHealthPotion( + item: ItemStack, + strong: Boolean, + ) { + item.editMeta(PotionMeta::class.java) { meta -> + meta.basePotionType = if (strong) PotionType.STRONG_HEALING else PotionType.HEALING + applyGenericItemMeta(meta) + } + } + + fun setRegen2Potion(item: ItemStack) { + item.editMeta(PotionMeta::class.java) { meta -> + applyGenericItemMeta(meta) + meta.basePotionType = PotionType.STRONG_REGENERATION + } + } + + fun setTurtleMasterPotion( + item: ItemStack, + long: Boolean, + strong: Boolean, + withName: Boolean = true, + ) { + val name = + when { + !withName -> null + !long && !strong -> "Turtle Master" + long -> "Long Turtle Master" + else -> "Strong Turtle Master" + } + return setCustomPotion( + item, + listOf( + PotionEffect(PotionEffectType.SLOWNESS, 20 * if (long) 40 else 20, if (strong) 5 else 3, false), + PotionEffect(PotionEffectType.RESISTANCE, 20 * if (long) 40 else 20, if (strong) 3 else 2, false), + ), + PotionEffectType.RESISTANCE.color, + name, + ) + } + + fun setLevitationPotion( + item: ItemStack, + level: Int, + withName: Boolean = true, + ) = setCustomPotion( + item, + PotionEffect(PotionEffectType.LEVITATION, 5 * 20, level, false), + PotionEffectType.LEVITATION.color, + if (withName) "Levitation" else null, + ) + + fun setBlindnessPotion( + item: ItemStack, + withName: Boolean = true, + ) = setCustomPotion( + item, + PotionEffect(PotionEffectType.BLINDNESS, 10 * 20, 0, false), + PotionEffectType.BLINDNESS.color, + if (withName) "Blindness" else null, + ) + + fun setPoisonPotion( + item: ItemStack, + level: Int, + withName: Boolean = true, + ) = setCustomPotion( + item, + PotionEffect(PotionEffectType.POISON, 20 * 20, level, false), + PotionEffectType.POISON.color, + if (withName) "Poison" else null, + ) + + fun setRegenPotion( + item: ItemStack, + withName: Boolean = true, + ) = setCustomPotion( + item, + PotionEffect(PotionEffectType.REGENERATION, 5 * 20, 4, false), + Color.fromRGB(205, 92, 171), + if (withName) "Regeneration" else null, + ) + + fun setSpeedPotion( + item: ItemStack, + withName: Boolean = true, + ) = setCustomPotion( + item, + PotionEffect(PotionEffectType.SPEED, 20 * 20, 1, false), + Color.fromRGB(51, 235, 255), + if (withName) "Speed" else null, + ) + + fun setJumpPotion( + item: ItemStack, + withName: Boolean = true, + ) = setCustomPotion( + item, + PotionEffect(PotionEffectType.JUMP_BOOST, 20 * 20, 3, false), + Color.fromRGB(253, 255, 132), + if (withName) "Jump Boost" else null, + ) + + fun applyGenericItemMeta(itemMeta: ItemMeta) { + itemMeta.apply { + isUnbreakable = true + addItemFlags(ItemFlag.HIDE_UNBREAKABLE) + addItemFlags(ItemFlag.HIDE_ADDITIONAL_TOOLTIP) + addItemFlags(ItemFlag.HIDE_ATTRIBUTES) + } + } + } + + init { + renderInventory() + } + + override fun getInventory(): Inventory = inventory + + fun onInventoryClick(event: InventoryClickEvent) { + val player = event.whoClicked + val slot = event.slot + + // check if we clicked on a page selector + if (slot in 27..35) { + when (slot) { + 29 -> category = HealthShopItemCategory.COMBAT + 30 -> category = HealthShopItemCategory.UTILITY + 32 -> category = HealthShopItemCategory.POTION + 33 -> category = HealthShopItemCategory.MISCELLANEOUS + } + renderInventory() + return + } + + // check if we clicked on a kit item + if (slot >= 36) { + val kitIndex = slot - 36 + val kit = kits.firstOrNull { it.index == kitIndex } + + if (!player.hasPermission("partygames.healthshop.kit.$kitIndex")) { + player.sendMessage( + mm.deserialize("You do not have permission to use this kit!"), + ) + player.playSound( + Sound.sound(Key.key("entity.villager.no"), Sound.Source.MASTER, 1.0f, 1.0f), + Sound.Emitter.self(), + ) + return + } + + if (kit == null && + purchasedItems.isNotEmpty() && + // don't save last used kit (index 8) + kitIndex != 8 + ) { + // save the kit + val kit = HealthShopKit(purchasedItems.toList(), kitIndex) + kits.add(kit) + databaseManager.saveHealthShopKit(playerUUID, kit) + renderKits() + } else if (kit != null) { + if (event.click.isRightClick && kitIndex != 8) { + // delete the kit + kits.remove(kit) + databaseManager.deleteHealthShopKit(playerUUID, kitIndex) + renderKits() + return + } + // load the kit + val currentItems = purchasedItems.toList() + for (item in currentItems) { + removeItem(item) + } + for (item in kit.items) { + addItem(item) + } + } + } + + val shopItem = + shopItems.firstOrNull { it.slot == slot && it.category == category } + ?: return // if the item is not found, do nothing + + // toggle purchased state + if (purchasedItems.contains(shopItem)) { + removeItem(shopItem) + } else { + try { + addItem(shopItem) + } catch (e: ShopFailedException) { + val message = + when (e.message) { + "no_healh" -> "You do not have enough hearts to purchase this item!" + "no_bow" -> "You must first buy a bow before purchasing this item!" + else -> "An error occurred while trying to purchase this item!" + } + player.sendMessage( + Component.text( + message, + NamedTextColor.RED, + ), + ) + player.playSound( + Sound.sound(Key.key("entity.villager.no"), Sound.Source.MASTER, 1.0f, 1.0f), + Sound.Emitter.self(), + ) + } + } + player.sendMessage( + MiniMessage + .miniMessage() + .deserialize("You have ${String.format("%.1f", player.health / 2.0)} ♥ left!"), + ) + } + + private fun renderInventory() { + inventory.clear() + for (item in shopItems.filter { it.category == category }) { + inventory.setItem( + item.slot, + item.item.clone().apply { + editMeta { meta -> + if (purchasedItems.contains(item)) { + meta.setEnchantmentGlintOverride(true) + val decorations = + mapOf( + TextDecoration.UNDERLINED to TextDecoration.State.TRUE, + TextDecoration.BOLD to TextDecoration.State.TRUE, + ) + meta.displayName((meta.displayName() ?: meta.itemName()).decorations(decorations)) + } + } + }, + ) + } + + // set up page selector + repeat(9) { i -> + val inventoryIndex = 27 + i + val item = + createBasicItem(Material.GRAY_STAINED_GLASS_PANE, "").apply { + editMeta { meta -> + meta.isHideTooltip = true + } + } + inventory.setItem(inventoryIndex, item) + } + for (category in HealthShopItemCategory.entries) { + val inventoryIndex = + when (category) { + HealthShopItemCategory.COMBAT -> 29 + HealthShopItemCategory.UTILITY -> 30 + HealthShopItemCategory.POTION -> 32 + HealthShopItemCategory.MISCELLANEOUS -> 33 + } + val item = + createBasicItem( + category.displayItem, + "${category.name.lowercase().replaceFirstChar { it.uppercase() }}", + ) + item.addItemFlags(ItemFlag.HIDE_ADDITIONAL_TOOLTIP, ItemFlag.HIDE_ATTRIBUTES) + inventory.setItem(inventoryIndex, item) + } + + renderKits() + } + + private fun renderKits() { + repeat(9) { i -> + val inventoryIndex = 36 + i + if (i != 8 && !player.hasPermission("partygames.healthshop.kit.$i")) { + val noPerms = + createBasicItem( + Material.BARRIER, + "Locked kit", + 1, + "You do not have permission to use this kit!", + ) + inventory.setItem(inventoryIndex, noPerms) + return@repeat + } + + val kit = kits.firstOrNull { it.index == i } + if (kit == null) { + val emptyKit = + createBasicItem( + Material.PAPER, + "Empty Kit", + 1, + if (i == 8) "Your last used kit will show up here" else "Click to save your current items as a kit!", + ).apply { + addItemFlags(ItemFlag.HIDE_ADDITIONAL_TOOLTIP) + } + inventory.setItem(inventoryIndex, emptyKit) + } else { + inventory.setItem(inventoryIndex, kit.getDisplayItem()) + } + } + } + + private fun removeItem(shopItem: HealthShopItem) { + if (!purchasedItems.remove(shopItem)) { + return + } + money += shopItem.price + + renderInventory() + + // special case: if we remove a bow, remove all arrows + if (shopItem.key == "bow") { + val arrowItem = purchasedItems.firstOrNull { it.group == "arrow" } + if (arrowItem != null) { + removeItem(arrowItem) + } + } + + player.health = money + } + + private fun addItem(shopItem: HealthShopItem) { + val sameCategory = purchasedItems.filter { it.group != "none" && it.group == shopItem.group } + // calculate how much money we'd have if we removed all the items in the same category + val moneyToAdd = sameCategory.sumOf { it.price } + // check if we have enough money + if ((money + moneyToAdd) <= shopItem.price) { + throw ShopFailedException("no_health") + } + // check if we're trying to buy an arrow + if (shopItem.group == "arrow") { + // check if we have a bow + if (!purchasedItems.any { it.key == "bow" }) { + throw ShopFailedException("no_bow") + } + } + purchasedItems.add(shopItem) + money -= shopItem.price + // play experience orb pickup sound + player.playSound( + Sound.sound(Key.key("entity.experience_orb.pickup"), Sound.Source.MASTER, 1.0f, 1.0f), + Sound.Emitter.self(), + ) + // remove all the items in the same category + sameCategory.forEach { removeItem(it) } + + renderInventory() + + player.health = money + } + + private fun addArmor( + player: Player, + armor: ArmorType, + ) { + // this ugly shit creates a list of armor items based on the armor type + // order: helmet, chestplate, leggings, boots + val armorItems = + when (armor) { + ArmorType.LEATHER -> + listOf( + ItemStack.of(Material.LEATHER_HELMET), + ItemStack.of(Material.LEATHER_CHESTPLATE), + ItemStack.of(Material.LEATHER_LEGGINGS), + ItemStack.of(Material.LEATHER_BOOTS), + ) + + ArmorType.CHAINMAIL -> + listOf( + ItemStack.of(Material.CHAINMAIL_HELMET), + ItemStack.of(Material.CHAINMAIL_CHESTPLATE), + ItemStack.of(Material.CHAINMAIL_LEGGINGS), + ItemStack.of(Material.CHAINMAIL_BOOTS), + ) + + ArmorType.IRON -> + listOf( + ItemStack.of(Material.IRON_HELMET), + ItemStack.of(Material.IRON_CHESTPLATE), + ItemStack.of(Material.IRON_LEGGINGS), + ItemStack.of(Material.CHAINMAIL_BOOTS), + ) + + ArmorType.DIAMOND -> + listOf( + ItemStack.of(Material.DIAMOND_HELMET), + ItemStack.of(Material.DIAMOND_CHESTPLATE), + ItemStack.of(Material.IRON_LEGGINGS), + ItemStack.of(Material.IRON_BOOTS), + ) + + ArmorType.NETHERITTE -> + listOf( + ItemStack.of(Material.NETHERITE_HELMET), + ItemStack.of(Material.NETHERITE_CHESTPLATE), + ItemStack.of(Material.DIAMOND_LEGGINGS), + ItemStack.of(Material.DIAMOND_BOOTS), + ) + } + armorItems.forEach { armorItem -> + armorItem.editMeta { meta -> + applyGenericItemMeta(meta) + if (purchasedItems.any { it.key == "protection_i" }) { + meta.addEnchant(Enchantment.PROTECTION, 1, true) + } + if (purchasedItems.any { it.key == "protection_ii" }) { + meta.addEnchant(Enchantment.PROTECTION, 2, true) + } + if (purchasedItems.any { it.key == "protection_iii" }) { + meta.addEnchant(Enchantment.PROTECTION, 3, true) + } + if (purchasedItems.any { it.key == "protection_iv" }) { + meta.addEnchant(Enchantment.PROTECTION, 4, true) + } + if (purchasedItems.any { it.key == "thorns" }) { + meta.addEnchant(Enchantment.THORNS, 2, true) + } + } + } + + player.inventory.setItem(EquipmentSlot.HEAD, armorItems[0]) + player.inventory.setItem(EquipmentSlot.CHEST, armorItems[1]) + player.inventory.setItem(EquipmentSlot.LEGS, armorItems[2]) + player.inventory.setItem(EquipmentSlot.FEET, armorItems[3]) + } + + fun giveItems() { + // process sword + val addSword = { material: Material -> + val sword = ItemStack.of(material) + sword.editMeta { meta -> + if (purchasedItems.any { it.key == "fire_aspect" }) { + meta.addEnchant(Enchantment.FIRE_ASPECT, 1, true) + } + purchasedItems.firstOrNull { it.key.startsWith("sharpness_") }.let { sharpnessItem -> + if (sharpnessItem != null) { + val sharpness = sharpnessItem.amount + meta.addEnchant(Enchantment.SHARPNESS, sharpness, true) + } + } + applyGenericItemMeta(meta) + } + player.inventory.addItem(sword) + } + kotlin + .runCatching { + purchasedItems.first { it.group == "sword" }.item.type + }.onSuccess { material -> + addSword(material) + }.onFailure { + addSword(Material.WOODEN_SWORD) + } + // process knockback stick + if (purchasedItems.any { it.key == "knockback_stick" }) { + val stick = ItemStack.of(Material.STICK) + stick.editMeta { meta -> + applyGenericItemMeta(meta) + meta.addEnchant(Enchantment.KNOCKBACK, 2, true) + } + player.inventory.addItem(stick) + } + // process shield + if (purchasedItems.any { it.key == "shield" }) { + val shield = ItemStack.of(Material.SHIELD) + shield.editMeta { meta -> + applyGenericItemMeta(meta) + } + player.inventory.setItem(EquipmentSlot.OFF_HAND, shield) + } + // process armor + kotlin + .runCatching { + purchasedItems.first { it.group == "armor" } + }.onSuccess { shopItem -> + when (shopItem.key) { + "chainmail_armor" -> addArmor(player, ArmorType.CHAINMAIL) + "iron_armor" -> addArmor(player, ArmorType.IRON) + "diamond_armor" -> addArmor(player, ArmorType.DIAMOND) + "netherite_armor" -> addArmor(player, ArmorType.NETHERITTE) + } + }.onFailure { + addArmor(player, ArmorType.LEATHER) + } + // process bow + if (purchasedItems.any { it.key == "bow" }) { + val bow = ItemStack.of(Material.BOW) + bow.editMeta { meta -> + applyGenericItemMeta(meta) + if (purchasedItems.any { it.key == "flame" }) { + meta.addEnchant(Enchantment.FLAME, 1, true) + } + if (purchasedItems.any { it.key == "power_i" }) { + meta.addEnchant(Enchantment.POWER, 1, true) + } + if (purchasedItems.any { it.key == "power_ii" }) { + meta.addEnchant(Enchantment.POWER, 2, true) + } + if (purchasedItems.any { it.key == "punch_i" }) { + meta.addEnchant(Enchantment.PUNCH, 1, true) + } + if (purchasedItems.any { it.key == "punch_ii" }) { + meta.addEnchant(Enchantment.PUNCH, 2, true) + } + } + player.inventory.addItem(bow) + } + // process arrows + kotlin + .runCatching { + purchasedItems.first { it.group == "arrow" } + }.onSuccess { shopItem -> + // 1 free arrow is included with the bow (which you need to buy an arrow) + playerData.maxArrows = shopItem.amount + 1 + } + + // process golden apples + purchasedItems.filter { it.group == "gap" }.forEach { item -> + val apple = ItemStack.of(Material.GOLDEN_APPLE, item.amount) + @Suppress("UnstableApiUsage") + if (item.key == "golden_apple_inf") { + // use the cooldown component for infinite golden apples + val cooldown = UseCooldown.useCooldown(10f).cooldownGroup(INF_GAP_COOLDOWN_KEY) + apple.setData(DataComponentTypes.USE_COOLDOWN, cooldown) + } + player.inventory.addItem(apple) + } + if (purchasedItems.any { it.key == "enchanted_golden_apple" }) { + val apple = ItemStack.of(Material.ENCHANTED_GOLDEN_APPLE, 1) + player.inventory.addItem(apple) + } + // process flint and steel + if (purchasedItems.any { it.key == "flint_and_steel" }) { + player.inventory.addItem(ItemStack.of(Material.FLINT_AND_STEEL, 1)) + } + // process oak planks + val oakPlanks = purchasedItems.firstOrNull { it.key == "oak_planks" } + if (oakPlanks != null) { + player.inventory.addItem(ItemStack.of(Material.OAK_PLANKS, oakPlanks.amount)) + } + // process fishing rod + if (purchasedItems.any { it.key == "fishing_rod" }) { + val fishingRod = ItemStack.of(Material.FISHING_ROD) + fishingRod.editMeta { meta -> + applyGenericItemMeta(meta) + } + player.inventory.addItem(fishingRod) + } + // process fireballs + val fireballItem = purchasedItems.firstOrNull { it.group == "fireball" } + if (fireballItem != null) { + val fireball = ItemStack.of(Material.FIRE_CHARGE, fireballItem.amount) + fireball.editMeta { meta -> + applyGenericItemMeta(meta) + } + player.inventory.addItem(fireball) + } + // process tnt + val tntItem = purchasedItems.firstOrNull { it.group == "tnt" } + if (tntItem != null) { + val tnt = ItemStack.of(Material.TNT, tntItem.amount) + tnt.editMeta { meta -> + applyGenericItemMeta(meta) + } + player.inventory.addItem(tnt) + } + // process totem of undying + if (purchasedItems.any { it.key == "totem_of_undying" }) { + val totem = ItemStack.of(Material.TOTEM_OF_UNDYING) + totem.editMeta { meta -> + applyGenericItemMeta(meta) + } + player.inventory.setItem(EquipmentSlot.OFF_HAND, totem) + } + // process ender pearls + val enderPearlItem = purchasedItems.firstOrNull { it.group == "ender_pearl" } + if (enderPearlItem != null) { + val enderPearl = ItemStack.of(Material.ENDER_PEARL, enderPearlItem.amount) + enderPearl.editMeta { meta -> + applyGenericItemMeta(meta) + } + player.inventory.addItem(enderPearl) + } + // process snow balls + if (purchasedItems.any { it.key == "snowball" }) { + val snowBall = ItemStack.of(Material.SNOWBALL, 16) + snowBall.editMeta { meta -> + applyGenericItemMeta(meta) + } + player.inventory.addItem(snowBall) + } + // process water bucket + if (purchasedItems.any { it.key == "water_bucket" }) { + val waterBucket = ItemStack.of(Material.WATER_BUCKET, 1) + waterBucket.editMeta { meta -> + applyGenericItemMeta(meta) + } + player.inventory.addItem(waterBucket) + } + + // process healing potions + for (purchasedPotion in purchasedItems.filter { it.key == "splash_healing" || it.key == "splash_healing_ii" }) { + val potion = ItemStack.of(Material.SPLASH_POTION, purchasedPotion.amount) + setHealthPotion(potion, purchasedPotion.key == "splash_healing") + player.inventory.addItem(potion) + } + // process regeneration potions + purchasedItems.firstOrNull { it.group == "regen_ii" }?.let { shopItem -> + val potion = ItemStack.of(Material.POTION) + setRegen2Potion(potion) + repeat(shopItem.amount) { + player.inventory.addItem(potion) + } + } + purchasedItems.firstOrNull { it.key == "regen_v" }?.let { shopItem -> + val potion = ItemStack.of(Material.POTION) + setRegenPotion(potion) + repeat(shopItem.amount) { + player.inventory.addItem(potion) + } + } + // process speed potion + purchasedItems.firstOrNull { it.group == "speed_ii" }?.let { shopItem -> + val potion = ItemStack.of(Material.POTION) + setSpeedPotion(potion) + repeat(shopItem.amount) { + player.inventory.addItem(potion) + } + } + // process jump potion + purchasedItems.firstOrNull { it.group == "jump_boost" }?.let { shopItem -> + val potion = ItemStack.of(Material.POTION) + setJumpPotion(potion) + repeat(shopItem.amount) { + player.inventory.addItem(potion) + } + } + // process turtle master potion + purchasedItems.firstOrNull { it.group == "turtle_master" }?.let { shopItem -> + val potion = ItemStack.of(Material.POTION) + val long = shopItem.key.endsWith("_long") + val strong = shopItem.key.endsWith("_strong") + setTurtleMasterPotion(potion, long, strong) + repeat(shopItem.amount) { + player.inventory.addItem(potion) + } + } + // process poison potion + purchasedItems.firstOrNull { it.group == "poison" }?.let { shopItem -> + val potion = ItemStack.of(Material.SPLASH_POTION) + setPoisonPotion(potion, if (shopItem.key == "poison_ii") 1 else 0) + repeat(shopItem.amount) { + player.inventory.addItem(potion) + } + } + // process blindness potion + purchasedItems.firstOrNull { it.group == "poison" }?.let { shopItem -> + val potion = ItemStack.of(Material.SPLASH_POTION) + setBlindnessPotion(potion) + repeat(shopItem.amount) { + player.inventory.addItem(potion) + } + } + // process levitation potion + purchasedItems.firstOrNull { it.group == "levitation" }?.let { shopItem -> + val potion = ItemStack.of(Material.SPLASH_POTION) + setLevitationPotion(potion, if (shopItem.key == "levitation_ii") 1 else 0) + repeat(shopItem.amount) { + player.inventory.addItem(potion) + } + } + + // process tracker + if (purchasedItems.any { it.key == "tracker" }) { + val tracker = ItemStack.of(Material.COMPASS) + tracker.editMeta { meta -> + meta.setEnchantmentGlintOverride(false) + } + player.inventory.addItem(tracker) + } + // process steal perk + if (purchasedItems.any { it.key == "steal_perk" }) { + playerData.stealPerk = true + } + // process heal perk + if (purchasedItems.any { it.key == "heal_perk" }) { + playerData.healPerk = true + } + // process double jump + if (purchasedItems.any { it.key == "double_jump" }) { + playerData.doubleJump = true + } + // process feather fall + if (purchasedItems.any { it.key == "feather_fall" }) { + playerData.featherFall = true + } + // process blast protection + if (purchasedItems.any { it.key == "blast_protection" }) { + playerData.blastProtection = true + } + + // save this kit (index 8 is the last used kit) + if (purchasedItems.isEmpty()) { + return + } + val kit = HealthShopKit(purchasedItems.toList(), 8) + databaseManager.saveHealthShopKit(playerUUID, kit) + } +} diff --git a/src/main/kotlin/info/mester/network/partygames/game/healthshop/SupplyChestTimer.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/SupplyChestTimer.kt similarity index 90% rename from src/main/kotlin/info/mester/network/partygames/game/healthshop/SupplyChestTimer.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/SupplyChestTimer.kt index 0402e47..84ccfce 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/healthshop/SupplyChestTimer.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/SupplyChestTimer.kt @@ -6,13 +6,13 @@ import java.util.function.Consumer import kotlin.math.exp import kotlin.random.Random -private const val STEEPNESS = 2.0 +private const val STEEPNESS = 2.05 class SupplyChestTimer( private val minigame: HealthShopMinigame, private val maxTime: Int, ) : Consumer { - private var offset = 0.0 + private var offset = -0.2 private var currentTime = 0 override fun accept(t: BukkitTask) { @@ -30,7 +30,7 @@ class SupplyChestTimer( val result = randomValue < functionValue // update the offset if (result) { - offset -= 0.035 // the next chest will have a lower chance of spawning + offset -= 0.055 // the next chest will have a lower chance of spawning minigame.spawnSupplyChest() } currentTime += 1 diff --git a/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/RideableSniffer.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/RideableSniffer.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/snifferhunt/RideableSniffer.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/RideableSniffer.kt diff --git a/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/SnifferHuntConfig.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/SnifferHuntConfig.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/snifferhunt/SnifferHuntConfig.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/SnifferHuntConfig.kt diff --git a/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/TreasureMap.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/TreasureMap.kt similarity index 97% rename from src/main/kotlin/info/mester/network/partygames/game/snifferhunt/TreasureMap.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/TreasureMap.kt index 4fe2464..74a7a24 100644 --- a/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/TreasureMap.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/snifferhunt/TreasureMap.kt @@ -1,5 +1,6 @@ package info.mester.network.partygames.game.snifferhunt +import info.mester.network.partygames.game.snifferhunt.TreasureMap.Companion.DAMPING_RADIUS import org.bukkit.util.noise.PerlinNoiseGenerator import kotlin.math.pow import kotlin.math.sqrt diff --git a/src/main/kotlin/info/mester/network/partygames/level/LevelData.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/level/LevelData.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/level/LevelData.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/level/LevelData.kt diff --git a/src/main/kotlin/info/mester/network/partygames/level/LevelManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/level/LevelManager.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/level/LevelManager.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/level/LevelManager.kt diff --git a/src/main/kotlin/info/mester/network/partygames/level/LevelPlaceholder.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/LevelPlaceholder.kt similarity index 79% rename from src/main/kotlin/info/mester/network/partygames/level/LevelPlaceholder.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/LevelPlaceholder.kt index bfe0ad4..f160139 100644 --- a/src/main/kotlin/info/mester/network/partygames/level/LevelPlaceholder.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/LevelPlaceholder.kt @@ -1,5 +1,7 @@ -package info.mester.network.partygames.level +package info.mester.network.partygames.placeholder +import info.mester.network.partygames.level.LevelData +import info.mester.network.partygames.level.LevelManager import me.clip.placeholderapi.expansion.PlaceholderExpansion import org.bukkit.entity.Player @@ -8,7 +10,7 @@ class LevelPlaceholder( ) : PlaceholderExpansion() { override fun getIdentifier() = "level" - override fun getAuthor() = "MesterNetwork" + override fun getAuthor() = "Party Games" override fun getVersion() = "1.0" diff --git a/src/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/PlayingPlaceholder.kt similarity index 66% rename from src/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/PlayingPlaceholder.kt index 7fbae59..11f1023 100644 --- a/src/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/PlayingPlaceholder.kt @@ -1,23 +1,31 @@ -package info.mester.network.partygames +package info.mester.network.partygames.placeholder -import info.mester.network.partygames.game.GameType +import info.mester.network.partygames.PartyGames +import info.mester.network.partygames.api.PartyGamesCore import me.clip.placeholderapi.expansion.PlaceholderExpansion import org.bukkit.entity.Player import java.util.concurrent.ConcurrentHashMap -class PlayingPlaceholder : PlaceholderExpansion() { +class PlayingPlaceholder( + plugin: PartyGames, +) : PlaceholderExpansion() { private val playingMap: MutableMap = ConcurrentHashMap() init { - val gameTypes = GameType.entries.map { it.name } - for (gameType in gameTypes) { - addPlaying(gameType, 0) + val bundles = + PartyGamesCore + .getInstance() + .gameRegistry + .getBundles() + .filter { it.plugin == plugin } + for (bundle in bundles) { + addPlaying(bundle.name, 0) } } override fun getIdentifier(): String = "playing" - override fun getAuthor(): String = "MesterNetwork" + override fun getAuthor(): String = "Party Games" override fun getVersion(): String = "1.0" diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/StatisticsPlaceholder.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/StatisticsPlaceholder.kt new file mode 100644 index 0000000..719e1cf --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/StatisticsPlaceholder.kt @@ -0,0 +1,62 @@ +package info.mester.network.partygames.placeholder + +import info.mester.network.partygames.DatabaseManager +import me.clip.placeholderapi.expansion.PlaceholderExpansion +import org.bukkit.entity.Player + +class StatisticsPlaceholder( + private val databaseManager: DatabaseManager, +) : PlaceholderExpansion() { + override fun getIdentifier(): String = "pgstat" + + override fun getAuthor(): String = "Party Games" + + override fun getVersion(): String = "1.0" + + private fun formatTime(seconds: Int): String { + val weeks = seconds / (7 * 24 * 60 * 60) + val days = (seconds % (7 * 24 * 60 * 60)) / (24 * 60 * 60) + val hours = (seconds % (24 * 60 * 60)) / (60 * 60) + val minutes = (seconds % (60 * 60)) / 60 + val remainingSeconds = seconds % 60 + // Build the formatted string, omitting zero values for brevity + return buildString { + if (weeks > 0) append("${weeks}w ") + if (days > 0) append("${days}d ") + if (hours > 0) append("${hours}h ") + if (minutes > 0) append("${minutes}m ") + if (remainingSeconds > 0 || isEmpty()) append("${remainingSeconds}s") // Always include seconds + }.trim() + } + + override fun onPlaceholderRequest( + player: Player?, + params: String, + ): String? { + if (player == null) { + return null + } + val arguments = params.lowercase().split("_") + if (arguments.getOrNull(0) == "gameswon") { + val game = arguments.getOrNull(1) + return databaseManager.getGamesWon(player.uniqueId, game).toString() + } + if (arguments.getOrNull(0) == "pointsgained") { + val game = arguments.getOrNull(1) + return databaseManager.getPointsGained(player.uniqueId, game).toString() + } + if (arguments.getOrNull(0) == "timeplayed") { + val isFormatted = arguments.getOrNull(arguments.size - 1) == "formatted" + val game = if (isFormatted && arguments.size == 2) null else arguments.getOrNull(1) + // If game is null or there's no "formatted" at the end, we use the correct game value + val timePlayed = databaseManager.getTimePlayed(player.uniqueId, game) + // If formatted is true, return the formatted time, else return the raw value + return if (isFormatted) { + formatTime(timePlayed) + } else { + timePlayed.toString() + } + } + return null + } +} diff --git a/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt similarity index 60% rename from src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt index d751a85..27b09a4 100644 --- a/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt @@ -1,7 +1,7 @@ package info.mester.network.partygames.sidebar -import info.mester.network.partygames.game.Game -import info.mester.network.partygames.game.GameState +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.GameState import info.mester.network.partygames.mm import info.mester.network.partygames.shorten import net.kyori.adventure.text.Component @@ -17,19 +17,24 @@ class GameSidebarComponent( drawable.drawLine(Component.empty()) val topList = game.topPlayers(8) drawable.drawLine(mm.deserialize("Top players:")) - for (i in 0 until 8) { + for (i in 0 until 3) { if (i >= topList.size) { break } val data = topList[i] - val player = data.first - val playerData = data.second - drawable.drawLine( - mm.deserialize( - // display the player's name in gray if they're offline - "${i + 1}. ${if (player.isOnline) player.name else "${player.name}"} - ${playerData.score}", - ), - ) + val player = data.player + val playerData = data.data + + val text = + buildString { + append("${i + 1}# ${if (player.isOnline) player.name else "${player.name}"} - ") + if (game.state == GameState.PLAYING) { + append("${playerData.score} (${playerData.stars}★)") + } else { + append("${playerData.stars}★") + } + } + drawable.drawLine(mm.deserialize(text)) } } @@ -45,6 +50,9 @@ class GameSidebarComponent( GameState.PRE_GAME -> { val minigame = game.runningMinigame!! drawable.drawLine(mm.deserialize("Loading: ").append(minigame.name)) + if (minigame.rootWorld.displayName != null) { + drawable.drawLine(mm.deserialize("Map: ${minigame.rootWorld.displayName}")) + } drawable.drawLine(mm.deserialize("Get ready!")) renderLeaderboard(drawable) } @@ -52,8 +60,12 @@ class GameSidebarComponent( GameState.PLAYING -> { val minigame = game.runningMinigame!! drawable.drawLine(mm.deserialize("Playing: ").append(minigame.name)) + if (minigame.rootWorld.displayName != null) { + drawable.drawLine(mm.deserialize("Map: ${minigame.rootWorld.displayName}")) + } drawable.drawLine(Component.empty()) - drawable.drawLine(mm.deserialize("Your score: ${game.playerData(player)!!.score}")) + drawable.drawLine(mm.deserialize("Your stars: ${game.playerData(player)!!.stars}★")) + drawable.drawLine(mm.deserialize("Your current score: ${game.playerData(player)!!.score}")) renderLeaderboard(drawable) } diff --git a/src/main/kotlin/info/mester/network/partygames/sidebar/LobbySidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/LobbySidebarComponent.kt similarity index 55% rename from src/main/kotlin/info/mester/network/partygames/sidebar/LobbySidebarComponent.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/LobbySidebarComponent.kt index 1c05fdb..389a4b7 100644 --- a/src/main/kotlin/info/mester/network/partygames/sidebar/LobbySidebarComponent.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/LobbySidebarComponent.kt @@ -2,10 +2,14 @@ package info.mester.network.partygames.sidebar import info.mester.network.partygames.level.LevelData import info.mester.network.partygames.mm +import me.clip.placeholderapi.PlaceholderAPI +import net.kyori.adventure.text.Component import net.megavex.scoreboardlibrary.api.sidebar.component.LineDrawable import net.megavex.scoreboardlibrary.api.sidebar.component.SidebarComponent +import org.bukkit.entity.Player class LobbySidebarComponent( + private val player: Player, private val levelData: LevelData, ) : SidebarComponent { override fun draw(drawable: LineDrawable) { @@ -28,5 +32,36 @@ class LobbySidebarComponent( } } drawable.drawLine(mm.deserialize(" <#777777>[$progressBar]")) + drawable.drawLine(Component.empty()) + drawable.drawLine( + mm.deserialize( + "Games Won: ${ + PlaceholderAPI.setPlaceholders( + player, + "%pgstat_gameswon%", + ) + }", + ), + ) + drawable.drawLine( + mm.deserialize( + "Points Gained: ${ + PlaceholderAPI.setPlaceholders( + player, + "%pgstat_pointsgained%", + ) + }", + ), + ) + drawable.drawLine( + mm.deserialize( + "Time Played: ${ + PlaceholderAPI.setPlaceholders( + player, + "%pgstat_timeplayed_formatted%", + ) + }", + ), + ) } } diff --git a/src/main/kotlin/info/mester/network/partygames/sidebar/QueueSidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/QueueSidebarComponent.kt similarity index 97% rename from src/main/kotlin/info/mester/network/partygames/sidebar/QueueSidebarComponent.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/QueueSidebarComponent.kt index e86f761..26ec10b 100644 --- a/src/main/kotlin/info/mester/network/partygames/sidebar/QueueSidebarComponent.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/QueueSidebarComponent.kt @@ -14,7 +14,7 @@ class QueueSidebarComponent( override fun draw(drawable: LineDrawable) { drawable.drawLine(mm.deserialize("<#777777>#${queue.id.shorten().substring(0..8)}")) drawable.drawLine(Component.empty()) - drawable.drawLine(mm.deserialize("Queuing for: ${queue.type.displayName}")) + drawable.drawLine(mm.deserialize("Queuing for: ${queue.bundle.displayName}")) drawable.drawLine(mm.deserialize("Players: ${queue.playerCount}/${queue.maxPlayers}")) if (queue.playerCount > 1) { drawable.drawLine(mm.deserialize("Ready: ${queue.readyPlayerCount}/${queue.playerCount}")) diff --git a/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt similarity index 91% rename from src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt index e5b4122..acafecd 100644 --- a/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt @@ -1,7 +1,7 @@ package info.mester.network.partygames.sidebar import info.mester.network.partygames.PartyGames -import info.mester.network.partygames.game.Game +import info.mester.network.partygames.api.Game import info.mester.network.partygames.game.Queue import info.mester.network.partygames.mm import net.megavex.scoreboardlibrary.api.sidebar.Sidebar @@ -33,7 +33,7 @@ class SidebarManager( private val plugin: PartyGames, ) { companion object { - private val title = SidebarComponent.staticLine(mm.deserialize("Party Games BETA")) + private val title = SidebarComponent.staticLine(mm.deserialize("Party Games")) private val dtf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss") private fun applyFooter(builder: SidebarComponent.Builder) { @@ -71,7 +71,7 @@ class SidebarManager( val builder = SidebarComponent .builder() - .addComponent(LobbySidebarComponent(plugin.levelManager.levelDataOf(player.uniqueId))) + .addComponent(LobbySidebarComponent(player, plugin.levelManager.levelDataOf(player.uniqueId))) applyFooter(builder) return ComponentSidebarLayout(title, builder.build()) } @@ -112,12 +112,12 @@ class SidebarManager( } fun openQueueSidebar(player: Player) { - val queue = plugin.gameManager.getQueueOf(player) ?: return + val queue = plugin.queueManager.getQueueOf(player) ?: return createSidebar(player, createQueueLayout(queue)) } fun openGameSidebar(player: Player) { - val game = plugin.gameManager.getGameOf(player) ?: return + val game = plugin.core.gameRegistry.getGameOf(player) ?: return createSidebar(player, createGameLayout(game, player)) } diff --git a/src/main/kotlin/info/mester/network/partygames/util/LocationUtils.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/LocationUtils.kt similarity index 90% rename from src/main/kotlin/info/mester/network/partygames/util/LocationUtils.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/LocationUtils.kt index c555951..65aab7d 100644 --- a/src/main/kotlin/info/mester/network/partygames/util/LocationUtils.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/LocationUtils.kt @@ -113,3 +113,17 @@ fun spreadPlayers( } } } + +fun snapTo90(angle: Float): Float { + // Normalize to -180 to 180 range + var normalized = ((angle + 180) % 360) - 180 + if (normalized < -180) normalized += 360 + // Snap to nearest 90 + return when { + normalized > 135 -> 180f + normalized > 45 -> 90f + normalized > -45 -> 0f + normalized > -135 -> -90f + else -> -180f + } +} diff --git a/src/main/kotlin/info/mester/network/partygames/util/WeightedItem.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/WeightedItem.kt similarity index 87% rename from src/main/kotlin/info/mester/network/partygames/util/WeightedItem.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/WeightedItem.kt index 6ed86d6..dc76411 100644 --- a/src/main/kotlin/info/mester/network/partygames/util/WeightedItem.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/WeightedItem.kt @@ -9,7 +9,7 @@ data class WeightedItem( fun List>.selectWeightedRandom(): T { val totalWeight = this.sumOf { it.weight } - val randomValue = Random(System.currentTimeMillis()).nextInt(totalWeight) + val randomValue = Random.nextInt(totalWeight) var currentWeight = 0 for (item in this) { currentWeight += item.weight diff --git a/pgame-plugin/src/main/resources/config.yml b/pgame-plugin/src/main/resources/config.yml new file mode 100644 index 0000000..3cf32d8 --- /dev/null +++ b/pgame-plugin/src/main/resources/config.yml @@ -0,0 +1,83 @@ +minigames: + healthshop: + worlds: + - world: mg-healthshop + x: 0.5 + y: 63.0 + z: 0.5 + - world: mg-healthshop2 + x: 0.5 + y: 65.0 + z: 0.5 + - world: mg-healthshop3 + x: 0.5 + y: 62.0 + z: 0.5 + class: info.mester.network.partygames.game.HealthShopMinigame + display-name: Health Shop v1.1 + speedbuilders: + worlds: + - world: mg-speedbuilders + x: 0.5 + y: 60.0 + z: 0.5 + class: info.mester.network.partygames.game.SpeedBuildersMinigame + display-name: Speed Builders + gardening: + worlds: + - world: mg-gardening + x: 0.5 + y: 65.0 + z: 0.5 + class: info.mester.network.partygames.game.GardeningMinigame + display-name: Gardening + damagedealer: + worlds: + - world: mg-damagedealer + x: 0.5 + y: 62.0 + z: 0.5 + class: info.mester.network.partygames.game.DamageDealerMinigame + display-name: Damage Dealer + snifferhunt: + worlds: + - world: mg-snifferhunt + x: 0.5 + y: 65.0 + z: 0.5 + class: info.mester.network.partygames.game.SnifferHuntMinigame + display-name: Sniffer Hunt + mineguessr: + worlds: + - world: mg-mineguessr + x: 8.0 + y: 35.0 + z: 4.0 + class: info.mester.network.partygames.game.MineguessrMinigame + display-name: Mine Guesser + gravjump: + worlds: + - world: mg-gravjump + x: 0.5 + y: 65.0 + z: 0.5 + yaw: -90.0 + class: info.mester.network.partygames.game.GravjumpMinigame + display-name: Gravjump +family-night: + - healthshop + - speedbuilders + - damagedealer + - mineguessr +save-interval: 10 +spawn-location: + ==: org.bukkit.Location + world: world + x: 78.5 + y: 93.0 + z: 40.5 + yaw: 90.0 + pitch: 0.0 +mineguessr: + world: world + max-size: 1000 \ No newline at end of file diff --git a/pgame-plugin/src/main/resources/gravjump.yml b/pgame-plugin/src/main/resources/gravjump.yml new file mode 100644 index 0000000..8b853b2 --- /dev/null +++ b/pgame-plugin/src/main/resources/gravjump.yml @@ -0,0 +1,27 @@ +# Where to place the start platform +start: + x: -3 + y: 59 + z: -7 + +# Where the wall starts +wall-from: + x: 3 + y: 65 + z: -3 + +# Where the wall ends +wall-to: + x: 3 + y: 68 + z: 3 + +# Where the first section starts +section-start: + x: 4 + y: 59 + z: -7 + +sections: + - 'plains' + - 'castle' \ No newline at end of file diff --git a/pgame-plugin/src/main/resources/gravjump/castle.nbt b/pgame-plugin/src/main/resources/gravjump/castle.nbt new file mode 100644 index 0000000..255014f Binary files /dev/null and b/pgame-plugin/src/main/resources/gravjump/castle.nbt differ diff --git a/pgame-plugin/src/main/resources/gravjump/plains.nbt b/pgame-plugin/src/main/resources/gravjump/plains.nbt new file mode 100644 index 0000000..eca25be Binary files /dev/null and b/pgame-plugin/src/main/resources/gravjump/plains.nbt differ diff --git a/pgame-plugin/src/main/resources/gravjump/start.nbt b/pgame-plugin/src/main/resources/gravjump/start.nbt new file mode 100644 index 0000000..7ba088a Binary files /dev/null and b/pgame-plugin/src/main/resources/gravjump/start.nbt differ diff --git a/pgame-plugin/src/main/resources/health-shop.yml b/pgame-plugin/src/main/resources/health-shop.yml new file mode 100644 index 0000000..4e163f7 --- /dev/null +++ b/pgame-plugin/src/main/resources/health-shop.yml @@ -0,0 +1,960 @@ +items: + # Combat Items + stone_sword: + id: stone_sword + name: Stone Sword + lore: + - A basic, but trustworthy stone sword. + - 9 out of 10 cavemen recommend it. + price: 8 + slot: 0 + group: sword + category: combat + iron_sword: + id: iron_sword + name: Iron Sword + lore: + - A sharp and durable iron sword. + - 10 out of 10 blacksmiths recommend it. + price: 12 + slot: 1 + group: sword + category: combat + diamond_sword: + id: diamond_sword + name: Diamond Sword + lore: + - A powerful and enduring diamond sword. + - A favorite among treasure hunters. + price: 16 + slot: 2 + group: sword + category: combat + netherite_sword: + id: netherite_sword + name: Netherite Sword + lore: + - The strongest sword in existence, imbued with ancient power. + - Only the bravest wield this mighty blade. + price: 20 + slot: 3 + group: sword + category: combat + + shield: + id: shield + name: Shield + lore: + - A basic shield, good for blocking attacks. + - Has a 1.5 second cooldown after use. + price: 3 + slot: 4 + group: offhand + category: combat + + knockback_stick: + id: stick + name: Knockback Stick + lore: + - A stick with Knockback II. + - Great if you want to be annoying. + price: 5 + slot: 5 + category: combat + + fire_aspect: + id: enchanted_book + name: Fire Aspect + lore: + - Let the world burn! + price: 10 + slot: 6 + category: combat + + sharpness_5: + id: enchanted_book + name: Sharpness V + lore: + - Increases your damage by 1.5♥ + - May cause a bit of pain. + price: 10 + slot: 7 + amount: 5 + group: sharpness + category: combat + sharpness_10: + id: enchanted_book + name: Sharpness X + lore: + - Increases your damage by 2.75♥ + - OUCH! + price: 18 + slot: 8 + amount: 10 + group: sharpness + category: combat + + chainmail_armor: + id: chainmail_chestplate + name: Chainmail Armor + lore: + - Surprisingly protective for interlinked metal rings. + - Medieval drip, now in PvP! + price: 8 + slot: 9 + group: armor + category: combat + iron_armor: + id: iron_chestplate + name: Iron Armor + lore: + - Reliable iron armor, forged for battle. + - Rusts slightly under pressure. + price: 12 + slot: 10 + group: armor + category: combat + diamond_armor: + id: diamond_chestplate + name: Diamond Armor + lore: + - Shiny, durable, and suspiciously flashy. + - Perfect for flexing and surviving. + price: 24 + slot: 11 + group: armor + category: combat + netherite_armor: + id: netherite_chestplate + name: Netherite Armor + lore: + - The pinnacle of armor technology. + - 'Warning: may cause overconfidence.' + price: 36 + slot: 12 + group: armor + category: combat + + thorns: + id: enchanted_book + name: Thorns + lore: + - Damages anyone who attacks you. + - Maybe don't use it on yourself. + price: 14 + slot: 13 + category: combat + protection_i: + id: enchanted_book + name: Protection I + lore: + - Gives you a 16% damage reduction. + - Every little bit helps, right? + price: 8 + slot: 14 + group: protection + category: combat + protection_ii: + id: enchanted_book + name: Protection II + lore: + - Gives you a 32% damage reduction. + - Now with 200% more smugness. + price: 16 + slot: 15 + amount: 2 + group: protection + category: combat + protection_iii: + id: enchanted_book + name: Protection III + lore: + - Gives you a 48% damage reduction. + - Just walk it off. You'll be fine. + price: 24 + slot: 16 + amount: 3 + group: protection + category: combat + protection_iv: + id: enchanted_book + name: Protection IV + lore: + - Gives you a 64% damage reduction. + - Like bubble wrap, but for warriors. + price: 32 + slot: 17 + amount: 4 + group: protection + category: combat + + bow: + id: bow + name: Bow + lore: + - A bow that shoots arrows. + - Comes with a regenerating arrow. + price: 8 + slot: 18 + category: combat + + arrow_1: + id: arrow + name: Arrow + lore: + - A single arrow for precise shots. + - Regenerates after 3 seconds. + price: 3 + slot: 19 + group: arrow + category: combat + arrow_2: + id: arrow + name: 2x Arrow + lore: + - Two arrows. Double the chances to whiff. + - Regenerates after 3 seconds. + price: 6 + slot: 20 + group: arrow + amount: 2 + category: combat + arrow_3: + id: arrow + name: 3x Arrow + lore: + - A polite way to say “leave.” + - Regenerates after 3 seconds. + price: 9 + slot: 21 + group: arrow + amount: 3 + category: combat + + flame: + id: enchanted_book + name: Flame + lore: + - Didn't they use flaming arrows in the past? + price: 10 + slot: 22 + category: combat + power_i: + id: enchanted_book + name: Power I + lore: + - Increases your damage by 50%, kinda hurts. + price: 8 + slot: 23 + group: power + category: combat + power_ii: + id: enchanted_book + name: Power II + lore: + - Increases your damage by 75%. + - Use it well, and you can one-shot someone. + price: 12 + slot: 24 + group: power + category: combat + punch_i: + id: enchanted_book + name: Punch I + lore: + - Send them flying! + price: 6 + slot: 25 + group: punch + category: combat + punch_ii: + id: enchanted_book + name: Punch II + lore: + - Can't touch this! + price: 10 + slot: 26 + group: punch + category: combat + + # Utility Items + golden_apple_1: + id: golden_apple + name: Golden Apple + lore: + - A single golden apple. + - It's a delicious treat! + price: 2 + slot: 0 + group: gap + category: utility + golden_apple_2: + id: golden_apple + name: 2x Golden Apple + lore: + - Two golden apples. + - Now we're talking! + price: 4 + slot: 1 + group: gap + amount: 2 + category: utility + golden_apple_3: + id: golden_apple + name: 3x Golden Apple + lore: + - Three golden apples. + - Enough regen for the whole family! + price: 6 + slot: 2 + group: gap + amount: 3 + category: utility + golden_apple_inf: + id: enchanted_golden_apple + name: Infinite Golden Apple + lore: + - Infinite golden apples. + - Regenerates every 10 seconds! + price: 14 + slot: 3 + group: gap + category: utility + enchanted_golden_apple: + id: enchanted_golden_apple + name: Enchanted Golden Apple + lore: + - The forbidden fruit of PvP. + - Eat this and become a mildly invincible war god. + price: 15 + slot: 4 + category: utility + + totem_of_undying: + id: totem_of_undying + name: Totem of Undying + lore: + - A totem that saves you from one death. + price: 15 + slot: 7 + amount: 1 + group: offhand + category: utility + + milk: + id: milk_bucket + name: Milk Bucket + lore: + - A bucket of milk. + - Removes all potion effects. + price: 5 + slot: 8 + amount: 1 + category: utility + + oak_planks: + id: oak_planks + name: 64x Oak Plank + lore: + - A basic oak plank. + - Good for building. + price: 3 + slot: 9 + amount: 64 + category: utility + + flint_and_steel: + id: flint_and_steel + name: Flint and Steel + lore: + - Sets things on fire. Intentionally. + - Technically not arson if it’s in a video game. + price: 5 + slot: 10 + amount: 1 + category: utility + + fishing_rod: + id: fishing_rod + name: Fishing Rod + lore: + - A fishing rod, great for fishing. + - But I think you'll use it to pull people towards you. + price: 4 + slot: 11 + amount: 1 + category: utility + + fireball: + id: fire_charge + name: Fireball + lore: + - A fireball, great for throwing at people. + - Just don't burn down the whole place, okay? + price: 4 + slot: 12 + amount: 1 + group: fireball + category: utility + fireball_2: + id: fire_charge + name: 2x Fireball + lore: + - Now with double the combustion. + - Because one fireball isn't annoying enough. + price: 8 + slot: 13 + amount: 2 + group: fireball + category: utility + fireball_3: + id: fire_charge + name: 3x Fireball + lore: + - You’ve officially entered the chaos bracket. + - Great for clearing bridges, friendships optional. + price: 12 + slot: 14 + amount: 3 + group: fireball + category: utility + + tnt: + id: tnt + name: TNT + lore: + - For when subtlety just isn't your thing. + - 'Warning: May attract unwanted attention.' + price: 4 + slot: 15 + amount: 1 + group: tnt + category: utility + tnt_2: + id: tnt + name: 2x TNT + lore: + - Double the boom, double the bad decisions. + - You're not here to make friends, are you? + price: 8 + slot: 16 + amount: 2 + group: tnt + category: utility + tnt_3: + id: tnt + name: 3x TNT + lore: + - A demolition plan disguised as strategy. + - They’ll never rebuild in time. + price: 12 + slot: 17 + amount: 3 + group: tnt + category: utility + + ender_pearl: + id: ender_pearl + name: Ender Pearl + lore: + - An ender pearl, great for teleporting. + - Just don't break the space-time continuum, please? + price: 6 + slot: 18 + amount: 1 + group: ender_pearl + category: utility + ender_pearl_2: + id: ender_pearl + name: 2x Ender Pearl + lore: + - Two ender pearls, great for teleporting twice I guess. + - Moderately annoying to your enemies. + price: 11 + slot: 19 + amount: 2 + group: ender_pearl + category: utility + ender_pearl_3: + id: ender_pearl + name: 3x Ender Pearl + lore: + - Three ender pearls, practically a new transportation system. + - Oh right, I guess it does have a cooldown. + price: 16 + slot: 20 + amount: 3 + group: ender_pearl + category: utility + ender_pearl_4: + id: ender_pearl + name: 4x Ender Pearl + lore: + - Basically an Enderman at this point. + - Do you even want to fight? + price: 21 + slot: 21 + amount: 4 + group: ender_pearl + category: utility + + snowball: + id: snowball + name: 16x Snowball + lore: + - A snowball, great for throwing at people. + - You can definitely lose friends with this. + price: 2 + slot: 22 + amount: 16 + category: utility + + water_bucket: + id: water_bucket + name: Water Bucket + lore: + - Stops fall damage, lava, and your enemies’ fun. + - The Swiss Army knife of Minecraft items. + price: 5 + slot: 26 + amount: 1 + category: utility + + # Potions + splash_healing_3: + id: splash_potion + name: 3x Splash Healing I + lore: + - Heals 2 instantly. + - Great for quick fixes and bad decisions. + price: 5 + slot: 0 + amount: 3 + group: splash_healing + category: potion + + splash_healing_5: + id: splash_potion + name: 5x Splash Healing I + lore: + - Heals 2 instantly. + - Enough to patch a party-wide disaster. + price: 8 + slot: 1 + amount: 5 + group: splash_healing + category: potion + + splash_healing_7: + id: splash_potion + name: 7x Splash Healing I + lore: + - Heals 2 instantly. + - Why stop panicking when you can heal mid-scream? + price: 11 + slot: 2 + amount: 7 + group: splash_healing + category: potion + + splash_healing_ii_3: + id: splash_potion + name: 3x Splash Healing II + lore: + - Heals 4 instantly. + - Basically a medical airstrike. + price: 9 + slot: 3 + amount: 3 + group: splash_healing_ii + category: potion + + splash_healing_ii_5: + id: splash_potion + name: 5x Splash Healing II + lore: + - Heals 4 instantly. + - Your enemies hate this one simple trick. + price: 14 + slot: 4 + amount: 5 + group: splash_healing_ii + category: potion + + splash_healing_ii_7: + id: splash_potion + name: 7x Splash Healing II + lore: + - Heals 4 instantly. + - May cause extreme survivability. + price: 19 + slot: 5 + amount: 7 + group: splash_healing_ii + category: potion + + regen_ii_2: + id: potion + name: 2x Regeneration II + lore: + - Restores 2 every 5 seconds. + - Slow but steady wins... maybe. + price: 6 + slot: 6 + amount: 2 + group: regen_ii + category: potion + + regen_ii_4: + id: potion + name: 4x Regeneration II + lore: + - Restores 2 every 5 seconds. + - Patience is a virtue. Healing is a luxury. + price: 10 + slot: 7 + amount: 4 + group: regen_ii + category: potion + + regen_v: + id: potion + name: 2x Regeneration V + lore: + - Restores 3 per second. + - Certified LEGA (Liquid Enchanted Golden Apples). + price: 14 + slot: 8 + amount: 2 + category: potion + + speed_ii_2: + id: potion + name: 2x Speed II + lore: + - Boosts your speed for 20 seconds. + - Why walk when you can run like a panic attack? + price: 4 + amount: 2 + slot: 9 + group: speed_ii + category: potion + + speed_ii_4: + id: potion + name: 4x Speed II + lore: + - Boosts your speed for 20 seconds. + - Basically caffeine in a bottle. + price: 6 + amount: 4 + slot: 10 + group: speed_ii + category: potion + + speed_ii_6: + id: potion + name: 6x Speed II + lore: + - Boosts your speed for 20 seconds. + - Wanna break the sound barrier? + price: 8 + amount: 6 + slot: 11 + group: speed_ii + category: potion + + jump_boost_2: + id: potion + name: 2x Jump Boost IV + lore: + - Leap up to 4 blocks for 20 seconds. + - Gravity? Never heard of her. + price: 4 + amount: 2 + slot: 12 + group: jump_boost + category: potion + + jump_boost_4: + id: potion + name: 4x Jump Boost IV + lore: + - Leap up to 4 blocks for 20 seconds. + - Kangaroos' favourite morning drink. + price: 6 + amount: 4 + slot: 13 + group: jump_boost + category: potion + + jump_boost_6: + id: potion + name: 6x Jump Boost IV + lore: + - Leap up to 4 blocks for 20 seconds. + - Built for players who fear stairs. + price: 8 + amount: 6 + slot: 14 + group: jump_boost + category: potion + + turtle_master: + id: potion + name: 2x Turtle Master + lore: + - Become invincible… eventually. + price: 8 + amount: 2 + slot: 15 + group: turtle_master + category: potion + turtle_master_long: + id: potion + name: 2x Long Turtle Master + lore: + - Same effects, now with 100% more suffering. + - Perfect for moving at glacial pace in style. + price: 12 + amount: 2 + slot: 16 + group: turtle_master + category: potion + turtle_master_strong: + id: potion + name: 2x Strong Turtle Master + lore: + - You are now a tank with two left feet. + price: 16 + amount: 2 + slot: 17 + group: turtle_master + category: potion + + poison_i: + id: splash_potion + name: Splash Poison I + lore: + - Deals 0.4 per second. + - A slow, satisfying descent into regret. + price: 7 + slot: 18 + amount: 1 + group: poison + category: potion + poison_ii: + id: splash_potion + name: Splash Poison II + lore: + - Deals 0.8 per second. + - The “why is this still happening” experience. + price: 10 + slot: 19 + amount: 1 + group: poison + category: potion + + blindness_1: + id: splash_potion + name: Splash Blindness + lore: + - Blinds for 10 seconds. + - Suddenly PvP becomes a horror game. + price: 6 + slot: 20 + amount: 1 + group: blindness + category: potion + blindness_2: + id: splash_potion + name: 2x Splash Blindness + lore: + - Blinds for 10 seconds. + - Double the disorientation, double the fun. + price: 11 + slot: 21 + amount: 2 + group: blindness + category: potion + + levitation_i: + id: splash_potion + name: Splash Levitation + lore: + - Lifts enemies for 5 seconds. + - Gravity is just a suggestion. + price: 8 + slot: 22 + amount: 1 + group: levitation + category: potion + levitation_ii: + id: splash_potion + name: Splash Levitation II + lore: + - Lifts enemies even higher for 5 seconds. + - “Where we’re going, we don’t need logic.” + price: 12 + slot: 23 + amount: 1 + group: levitation + category: potion + + # Miscellaneous Items + tracker: + id: compass + name: Tracker + lore: + - Right click to point to the nearest player. + - Has a 5-second cooldown and alerts the tracked player. + price: 6 + slot: 0 + category: miscellaneous + + steal_perk: + id: bundle + name: Steal Perk + lore: + - You will get every item from the players you kill. + - The perk can only be used once. Very high risk, high reward. + price: 20 + slot: 1 + group: perk + category: miscellaneous + heal_perk: + id: red_dye + name: Heal Perk + lore: + - You will heal to full health when you kill a player. + - The perk is kept after use. + price: 7 + slot: 2 + group: perk + category: miscellaneous + double_jump: + id: rabbit_foot + name: Double Jump + lore: + - Gives you the ability to jump twice. + - The perk is kept after use and has a 3 second cooldown. + price: 10 + slot: 3 + group: perk + category: miscellaneous + feather_fall: + id: feather + name: Feather Fall + lore: + - You will not take fall damage. + price: 6 + slot: 4 + group: perk + category: miscellaneous + blast_protection: + id: nether_star + name: Blast Protection + lore: + - You will not take damage from explosions. + - Great for when you want to be a walking bomb shelter. + price: 8 + slot: 5 + group: perk + category: miscellaneous + +spawn-locations: + 0: + - x: 52.5 + y: 64.0 + z: -51.5 + - x: 13.5 + y: 69.0 + z: -58.5 + - x: -48.5 + y: 65.0 + z: -43.5 + - x: -48.5 + y: 64.0 + z: 22.5 + - x: -26.5 + y: 77.0 + z: 54.5 + - x: 49.5 + y: 77.0 + z: 45.5 + - x: 48.5 + y: 67.0 + z: 3.5 + - x: -0.5 + y: 64.0 + z: -0.5 + 1: + - x: 36.5 + y: 56.0 + z: 36.5 + - x: 28.5 + y: 58.0 + z: 28.5 + - x: -34.5 + y: 56.0 + z: 36.5 + - x: -27.5 + y: 58.0 + z: 29.5 + - x: -34.5 + y: 56.0 + z: -35.5 + - x: -27.5 + y: 58.0 + z: -28.5 + - x: 35.5 + y: 56.0 + z: -35.5 + - x: 28.5 + y: 58.0 + z: -28.5 + 2: + - x: 28.5 + y: 63.0 + z: 53.5 + yaw: 180.0 + - x: -34.5 + y: 63.0 + z: 45.5 + yaw: -135.0 + - x: -31.5 + y: 63.0 + z: 19.5 + yaw: -90.0 + - x: -42.5 + y: 63.0 + z: -48.5 + yaw: -45.0 + - x: -26.5 + y: 63.0 + z: -22.5 + yaw: -90.0 + - x: -1.5 + y: 63.0 + z: 2.5 + yaw: -90.0 + - x: 35.5 + y: 63.0 + z: -11.5 + yaw: 90.0 + - x: 48.5 + y: 63.0 + z: -49.5 + yaw: 45.0 +supply-drops: + - key: golden_apple_1 + weight: 60 + - key: golden_apple_2 + weight: 20 + - key: golden_apple_3 + weight: 5 + - key: jump_potion + weight: 15 + - key: regen_potion + weight: 1 +health: 80 \ No newline at end of file diff --git a/src/main/resources/paper-plugin.yml b/pgame-plugin/src/main/resources/paper-plugin.yml similarity index 67% rename from src/main/resources/paper-plugin.yml rename to pgame-plugin/src/main/resources/paper-plugin.yml index 02aa864..4f8c381 100644 --- a/src/main/resources/paper-plugin.yml +++ b/pgame-plugin/src/main/resources/paper-plugin.yml @@ -1,17 +1,16 @@ name: PartyGames -version: 'a1.0' +version: '1.0' main: info.mester.network.partygames.PartyGames api-version: '1.21' bootstrapper: info.mester.network.partygames.Bootstrapper -loader: info.mester.network.partygames.Loader load: POSTWORLD dependencies: server: - WorldEdit: + PartyGamesCore: load: BEFORE required: true join-classpath: true - ViaVersion: + FastAsyncWorldEdit: load: BEFORE required: true join-classpath: true @@ -19,6 +18,17 @@ dependencies: load: BEFORE required: true join-classpath: true + My_Worlds: + load: BEFORE + required: false + FriendsAPIForPartyAndFriends: + load: BEFORE + required: false + join-classpath: true + Spigot-Party-API-PAF: + load: BEFORE + required: false + join-classpath: true authors: - Mester description: The paper plugin for MesterNetwork's Party Games diff --git a/src/main/resources/sniffer-hunt.yml b/pgame-plugin/src/main/resources/sniffer-hunt.yml similarity index 100% rename from src/main/resources/sniffer-hunt.yml rename to pgame-plugin/src/main/resources/sniffer-hunt.yml diff --git a/pgame-plugin/src/main/resources/speed-builders.yml b/pgame-plugin/src/main/resources/speed-builders.yml new file mode 100644 index 0000000..c45030b --- /dev/null +++ b/pgame-plugin/src/main/resources/speed-builders.yml @@ -0,0 +1,100 @@ +structures: + # Easy difficulty + portal: + difficulty: EASY + display_name: Portal + bed: + difficulty: EASY + display_name: Bed + farm: + difficulty: EASY + display_name: Farm + japanese_idk: + difficulty: EASY + display_name: Japanese Arch Thingy (?) + well: + difficulty: EASY + display_name: Well, well, well + first_house: + difficulty: EASY + display_name: First House + herobrine: + difficulty: EASY + display_name: Herobrine + kitchen: + difficulty: EASY + display_name: Kitchen + drug_farm: + difficulty: EASY + display_name: Drug Farm (very legal, trust) + animal_farm: + difficulty: EASY + display_name: Animal Farm + village: + difficulty: EASY + display_name: Village + stronghold: + difficulty: EASY + display_name: Stronghold + bordem: + difficulty: EASY + display_name: Bordem + # Medium difficulty + bookshelves: + difficulty: MEDIUM + display_name: Bookshelves & Enchanting Table + dragon: + difficulty: MEDIUM + display_name: Dragon + bigportal: + difficulty: MEDIUM + display_name: Big Portal + warrior: + difficulty: MEDIUM + display_name: Warrior + cart: + difficulty: MEDIUM + display_name: Cart + botanic: + difficulty: MEDIUM + display_name: Botanic Garden + witch_hut: + difficulty: MEDIUM + display_name: Witch Hut + # Hard difficulty + enchanting: + difficulty: HARD + display_name: Enchanting Room + colors: + difficulty: HARD + display_name: Colors + tree: + difficulty: HARD + display_name: Tree + arcade: + difficulty: HARD + display_name: Arcade + boat: + difficulty: HARD + display_name: Boat + car: + difficulty: HARD + display_name: Car + graveyard: + difficulty: HARD + display_name: Graveyard + icecaps: + difficulty: HARD + display_name: Ice Caps + # Insane difficulty + outpost: + difficulty: INSANE + display_name: Outpost + end_city: + difficulty: INSANE + display_name: End City +scores: + EASY: 10 + MEDIUM: 15 + HARD: 25 + INSANE: 40 \ No newline at end of file diff --git a/pgame-plugin/src/main/resources/speedbuilders/.gitignore b/pgame-plugin/src/main/resources/speedbuilders/.gitignore new file mode 100644 index 0000000..1ef548e --- /dev/null +++ b/pgame-plugin/src/main/resources/speedbuilders/.gitignore @@ -0,0 +1,3 @@ +speedbuilders_template.nbt +structure_template.nbt +entity_test.nbt \ No newline at end of file diff --git a/pgame-plugin/src/main/resources/speedbuilders/animal_farm.nbt b/pgame-plugin/src/main/resources/speedbuilders/animal_farm.nbt new file mode 100644 index 0000000..ae244c0 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/animal_farm.nbt differ diff --git a/src/main/resources/speedbuilders/arcade.nbt b/pgame-plugin/src/main/resources/speedbuilders/arcade.nbt similarity index 100% rename from src/main/resources/speedbuilders/arcade.nbt rename to pgame-plugin/src/main/resources/speedbuilders/arcade.nbt diff --git a/src/main/resources/speedbuilders/bed.nbt b/pgame-plugin/src/main/resources/speedbuilders/bed.nbt similarity index 100% rename from src/main/resources/speedbuilders/bed.nbt rename to pgame-plugin/src/main/resources/speedbuilders/bed.nbt diff --git a/src/main/resources/speedbuilders/bigportal.nbt b/pgame-plugin/src/main/resources/speedbuilders/bigportal.nbt similarity index 100% rename from src/main/resources/speedbuilders/bigportal.nbt rename to pgame-plugin/src/main/resources/speedbuilders/bigportal.nbt diff --git a/src/main/resources/speedbuilders/boat.nbt b/pgame-plugin/src/main/resources/speedbuilders/boat.nbt similarity index 100% rename from src/main/resources/speedbuilders/boat.nbt rename to pgame-plugin/src/main/resources/speedbuilders/boat.nbt diff --git a/src/main/resources/speedbuilders/bookshelves.nbt b/pgame-plugin/src/main/resources/speedbuilders/bookshelves.nbt similarity index 100% rename from src/main/resources/speedbuilders/bookshelves.nbt rename to pgame-plugin/src/main/resources/speedbuilders/bookshelves.nbt diff --git a/pgame-plugin/src/main/resources/speedbuilders/bordem.nbt b/pgame-plugin/src/main/resources/speedbuilders/bordem.nbt new file mode 100644 index 0000000..8264e78 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/bordem.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/botanic.nbt b/pgame-plugin/src/main/resources/speedbuilders/botanic.nbt new file mode 100644 index 0000000..8f264bc Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/botanic.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/car.nbt b/pgame-plugin/src/main/resources/speedbuilders/car.nbt new file mode 100644 index 0000000..b1aa347 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/car.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/cart.nbt b/pgame-plugin/src/main/resources/speedbuilders/cart.nbt new file mode 100644 index 0000000..6327f80 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/cart.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/colors.nbt b/pgame-plugin/src/main/resources/speedbuilders/colors.nbt new file mode 100644 index 0000000..d343512 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/colors.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/dragon.nbt b/pgame-plugin/src/main/resources/speedbuilders/dragon.nbt new file mode 100644 index 0000000..a3cf41e Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/dragon.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/drug_farm.nbt b/pgame-plugin/src/main/resources/speedbuilders/drug_farm.nbt new file mode 100644 index 0000000..c72fc3d Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/drug_farm.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/enchanting.nbt b/pgame-plugin/src/main/resources/speedbuilders/enchanting.nbt new file mode 100644 index 0000000..3457115 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/enchanting.nbt differ diff --git a/src/main/resources/speedbuilders/end_city.nbt b/pgame-plugin/src/main/resources/speedbuilders/end_city.nbt similarity index 100% rename from src/main/resources/speedbuilders/end_city.nbt rename to pgame-plugin/src/main/resources/speedbuilders/end_city.nbt diff --git a/src/main/resources/speedbuilders/farm.nbt b/pgame-plugin/src/main/resources/speedbuilders/farm.nbt similarity index 100% rename from src/main/resources/speedbuilders/farm.nbt rename to pgame-plugin/src/main/resources/speedbuilders/farm.nbt diff --git a/pgame-plugin/src/main/resources/speedbuilders/first_house.nbt b/pgame-plugin/src/main/resources/speedbuilders/first_house.nbt new file mode 100644 index 0000000..4145508 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/first_house.nbt differ diff --git a/src/main/resources/speedbuilders/graveyard.nbt b/pgame-plugin/src/main/resources/speedbuilders/graveyard.nbt similarity index 100% rename from src/main/resources/speedbuilders/graveyard.nbt rename to pgame-plugin/src/main/resources/speedbuilders/graveyard.nbt diff --git a/pgame-plugin/src/main/resources/speedbuilders/herobrine.nbt b/pgame-plugin/src/main/resources/speedbuilders/herobrine.nbt new file mode 100644 index 0000000..7cb5ddd Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/herobrine.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/icecaps.nbt b/pgame-plugin/src/main/resources/speedbuilders/icecaps.nbt new file mode 100644 index 0000000..889b9c9 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/icecaps.nbt differ diff --git a/src/main/resources/speedbuilders/japanese_idk.nbt b/pgame-plugin/src/main/resources/speedbuilders/japanese_idk.nbt similarity index 100% rename from src/main/resources/speedbuilders/japanese_idk.nbt rename to pgame-plugin/src/main/resources/speedbuilders/japanese_idk.nbt diff --git a/pgame-plugin/src/main/resources/speedbuilders/jungle.nbt b/pgame-plugin/src/main/resources/speedbuilders/jungle.nbt new file mode 100644 index 0000000..6ce8ebf Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/jungle.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/kitchen.nbt b/pgame-plugin/src/main/resources/speedbuilders/kitchen.nbt new file mode 100644 index 0000000..798570b Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/kitchen.nbt differ diff --git a/src/main/resources/speedbuilders/mushroom.nbt b/pgame-plugin/src/main/resources/speedbuilders/mushroom.nbt similarity index 100% rename from src/main/resources/speedbuilders/mushroom.nbt rename to pgame-plugin/src/main/resources/speedbuilders/mushroom.nbt diff --git a/src/main/resources/speedbuilders/outpost.nbt b/pgame-plugin/src/main/resources/speedbuilders/outpost.nbt similarity index 100% rename from src/main/resources/speedbuilders/outpost.nbt rename to pgame-plugin/src/main/resources/speedbuilders/outpost.nbt diff --git a/src/main/resources/speedbuilders/portal.nbt b/pgame-plugin/src/main/resources/speedbuilders/portal.nbt similarity index 100% rename from src/main/resources/speedbuilders/portal.nbt rename to pgame-plugin/src/main/resources/speedbuilders/portal.nbt diff --git a/pgame-plugin/src/main/resources/speedbuilders/stronghold.nbt b/pgame-plugin/src/main/resources/speedbuilders/stronghold.nbt new file mode 100644 index 0000000..6d066ca Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/stronghold.nbt differ diff --git a/src/main/resources/speedbuilders/tree.nbt b/pgame-plugin/src/main/resources/speedbuilders/tree.nbt similarity index 100% rename from src/main/resources/speedbuilders/tree.nbt rename to pgame-plugin/src/main/resources/speedbuilders/tree.nbt diff --git a/pgame-plugin/src/main/resources/speedbuilders/village.nbt b/pgame-plugin/src/main/resources/speedbuilders/village.nbt new file mode 100644 index 0000000..9853dc6 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/village.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/warrior.nbt b/pgame-plugin/src/main/resources/speedbuilders/warrior.nbt new file mode 100644 index 0000000..0c5e857 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/warrior.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/well.nbt b/pgame-plugin/src/main/resources/speedbuilders/well.nbt new file mode 100644 index 0000000..f57d7fa Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/well.nbt differ diff --git a/pgame-plugin/src/main/resources/speedbuilders/witch_hut.nbt b/pgame-plugin/src/main/resources/speedbuilders/witch_hut.nbt new file mode 100644 index 0000000..bfec9f9 Binary files /dev/null and b/pgame-plugin/src/main/resources/speedbuilders/witch_hut.nbt differ diff --git a/src/sniffer-hunt-schema.json b/pgame-plugin/src/sniffer-hunt-schema.json similarity index 100% rename from src/sniffer-hunt-schema.json rename to pgame-plugin/src/sniffer-hunt-schema.json diff --git a/pgame-plugin/src/speed-builders-schema.json b/pgame-plugin/src/speed-builders-schema.json new file mode 100644 index 0000000..185ff6d --- /dev/null +++ b/pgame-plugin/src/speed-builders-schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "structures": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9_]+$": { + "type": "object", + "properties": { + "difficulty": { + "type": "string", + "description": "Difficulty of the structure.", + "enum": [ + "EASY", + "MEDIUM", + "HARD", + "INSANE" + ] + }, + "display_name": { + "type": "string", + "description": "Display name of the structure." + } + }, + "required": [ + "difficulty", + "display_name" + ], + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "scores": { + "type": "object", + "properties": { + "EASY": { + "type": "integer", + "description": "Score for an EASY difficulty minigame." + }, + "MEDIUM": { + "type": "integer", + "description": "Score for a MEDIUM difficulty minigame." + }, + "HARD": { + "type": "integer", + "description": "Score for a HARD difficulty minigame." + }, + "INSANE": { + "type": "integer", + "description": "Score for an INSANE difficulty minigame." + } + }, + "additionalProperties": false, + "required": [ + "EASY", + "MEDIUM", + "HARD", + "INSANE" + ] + } + }, + "required": [ + "structures", + "scores" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/test/kotlin/info/mester/network/partygames/TestPartyGames.kt b/pgame-plugin/src/test/kotlin/info/mester/network/partygames/TestPartyGames.kt similarity index 100% rename from src/test/kotlin/info/mester/network/partygames/TestPartyGames.kt rename to pgame-plugin/src/test/kotlin/info/mester/network/partygames/TestPartyGames.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index d90944d..b8817c3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,4 +5,8 @@ pluginManagement { gradlePluginPortal() maven("https://repo.papermc.io/repository/maven-public/") } -} \ No newline at end of file +} + +include("pgame-api") +include("pgame-plugin") +include("test-minigame") diff --git a/src/config-schema.json b/src/config-schema.json deleted file mode 100644 index 35995ed..0000000 --- a/src/config-schema.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft-07/schema", - "type": "object", - "properties": { - "locations": { - "type": "object", - "properties": { - "minigames": { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9_]+$": { - "type": "object", - "properties": { - "worlds": { - "type": "array", - "items": { - "type": "string" - } - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "z": { - "type": "number" - } - }, - "required": [ - "worlds", - "x", - "y", - "z" - ] - } - }, - "additionalProperties": false - } - }, - "required": [ - "minigames" - ] - }, - "steve-skin": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "signature": { - "type": "string" - } - }, - "required": [ - "value", - "signature" - ] - }, - "save-interval": { - "type": "integer", - "minimum": 1 - }, - "spawn-location": { - "type": "object", - "properties": { - "world": { - "type": "string" - }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "z": { - "type": "number" - }, - "yaw": { - "type": "number" - }, - "pitch": { - "type": "number" - } - }, - "required": [ - "world", - "x", - "y", - "z", - "yaw", - "pitch" - ] - } - }, - "required": [ - "locations", - "steve-skin", - "save-interval", - "spawn-location" - ], - "additionalProperties": false -} \ No newline at end of file diff --git a/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt b/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt deleted file mode 100644 index de5fda1..0000000 --- a/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt +++ /dev/null @@ -1,52 +0,0 @@ -package info.mester.network.partygames - -import info.mester.network.partygames.level.LevelData -import java.io.File -import java.sql.Connection -import java.sql.DriverManager -import java.util.UUID - -class DatabaseManager( - databaseFile: File, -) { - private val connection: Connection - - init { - // init JDBC driver - Class.forName("org.sqlite.JDBC") - // create database file - if (!databaseFile.exists()) { - databaseFile.createNewFile() - } - // connect to database - connection = DriverManager.getConnection("jdbc:sqlite:${databaseFile.absolutePath}") - // create tables if they don't exist - connection.createStatement().use { statement -> - statement.executeUpdate("CREATE TABLE IF NOT EXISTS levels (uuid CHAR(32) PRIMARY KEY, level INTEGER, exp INTEGER)") - } - } - - fun getLevel(uuid: UUID): LevelData? { - val statement = connection.prepareStatement("SELECT * FROM levels WHERE uuid = ?") - statement.setString(1, uuid.shorten()) - val resultSet = statement.executeQuery() - if (resultSet.next()) { - val level = resultSet.getInt("level") - val exp = resultSet.getInt("exp") - return LevelData(level, exp) - } else { - return null - } - } - - fun saveLevel( - uuid: UUID, - levelData: LevelData, - ) { - val statement = connection.prepareStatement("INSERT OR REPLACE INTO levels (uuid, level, exp) VALUES (?, ?, ?)") - statement.setString(1, uuid.shorten()) - statement.setInt(2, levelData.level) - statement.setInt(3, levelData.xp) - statement.executeUpdate() - } -} diff --git a/src/main/kotlin/info/mester/network/partygames/Loader.kt b/src/main/kotlin/info/mester/network/partygames/Loader.kt deleted file mode 100644 index 075e94d..0000000 --- a/src/main/kotlin/info/mester/network/partygames/Loader.kt +++ /dev/null @@ -1,25 +0,0 @@ -package info.mester.network.partygames - -import io.papermc.paper.plugin.loader.PluginClasspathBuilder -import io.papermc.paper.plugin.loader.PluginLoader -import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver -import org.eclipse.aether.artifact.DefaultArtifact -import org.eclipse.aether.graph.Dependency -import org.eclipse.aether.repository.RemoteRepository - -@Suppress("UnstableApiUsage", "unused") -class Loader : PluginLoader { - override fun classloader(classpathBuilder: PluginClasspathBuilder) { - val resolver = MavenLibraryResolver() - resolver.addRepository( - RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build(), - ) - resolver.addRepository( - RemoteRepository - .Builder("rapture", "default", "https://repo.rapture.pw/repository/maven-releases/") - .build(), - ) - resolver.addDependency(Dependency(DefaultArtifact("com.squareup.okhttp3:okhttp:4.12.0"), null)) - classpathBuilder.addLibrary(resolver) - } -} diff --git a/src/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt b/src/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt deleted file mode 100644 index 188d290..0000000 --- a/src/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt +++ /dev/null @@ -1,105 +0,0 @@ -package info.mester.network.partygames.admin - -import com.google.gson.JsonParser -import com.mojang.authlib.properties.Property -import info.mester.network.partygames.PartyGames -import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket -import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket -import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket -import org.bukkit.Bukkit -import org.bukkit.craftbukkit.entity.CraftPlayer -import org.bukkit.entity.Player -import java.io.InputStreamReader -import java.net.URI - -fun updateVisibilityOfPlayer( - playerToChange: Player, - visible: Boolean, -) { - // change the player's visibility for everyone who isn't an admin - for (player in Bukkit - .getOnlinePlayers() - .filter { it.uniqueId != playerToChange.uniqueId } - .filter { - !PartyGames.plugin.isAdmin(it) - }) { - if (visible) { - player.hidePlayer(PartyGames.plugin, playerToChange) - } else { - player.showPlayer(PartyGames.plugin, playerToChange) - } - } -} - -enum class SkinType { - STEVE, - OWN, -} - -fun changePlayerSkin( - player: Player, - skinType: SkinType, -) { - val plugin = PartyGames.plugin - val entityPlayer = (player as CraftPlayer).handle - val profile = entityPlayer.gameProfile - // first we have to decide the skin property - var skinProperty = - Property( - "textures", - plugin.config.getString("steve-skin.value")!!, - plugin.config.getString("steve-skin.signature")!!, - ) - if (skinType == SkinType.OWN) { - // let's ask Mojang for the player's skin - // if we fail (most likely on offline mode), use the default skin - val uuid = player.uniqueId.toString().replace("-", "") - runCatching { - val url = - URI("https://sessionserver.mojang.com/session/minecraft/profile/$uuid?unsigned=false").toURL() - val reader = InputStreamReader(url.openStream()) - val properties = - JsonParser - .parseReader(reader) - .asJsonObject - .get("properties") - .asJsonArray - .get(0) - .asJsonObject - val value = properties.get("value").asString - val signature = properties.get("signature").asString - skinProperty = Property("textures", value, signature) - }.onFailure { - plugin.logger.warning("Failed to get skin for ${player.name} ($uuid), are we in offline mode?") - } - } - profile.properties.clear() - profile.properties.put("textures", skinProperty) - val removeInfo = ClientboundPlayerInfoRemovePacket(listOf(entityPlayer.uuid)) - val addInfo = - ClientboundPlayerInfoUpdatePacket( - ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, - entityPlayer, - ) - val destroyEntity = ClientboundRemoveEntitiesPacket(entityPlayer.id) - Bukkit - .getOnlinePlayers() - // admins should still see their skin - .filter { - it.uniqueId != player.uniqueId && !plugin.isAdmin(it) - }.forEach { onlinePlayer -> - val connection = (onlinePlayer as CraftPlayer).handle.connection - connection.send(removeInfo) - connection.send(destroyEntity) - connection.send(addInfo) - // force the player to re-render - Bukkit.getScheduler().runTaskLater( - plugin, - Runnable { - onlinePlayer.hidePlayer(plugin, player) - onlinePlayer.showPlayer(plugin, player) - }, - 1, - ) - } -} diff --git a/src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt b/src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt deleted file mode 100644 index 5d1b7d6..0000000 --- a/src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt +++ /dev/null @@ -1,121 +0,0 @@ -package info.mester.network.partygames.admin - -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName -import info.mester.network.partygames.PartyGames -import info.mester.network.partygames.game.Game -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.format.TextDecoration -import okhttp3.Request -import org.bukkit.Bukkit -import org.bukkit.Material -import org.bukkit.entity.Player -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.inventory.Inventory -import org.bukkit.inventory.InventoryHolder -import org.bukkit.inventory.ItemStack - -data class PackInfo( - val packs: List, -) - -data class Pack( - val id: String, - val icon: String, - @SerializedName("short_name") val shortName: String, - @SerializedName("friendly_name") val friendlyName: String, - val description: String, - val downloads: Downloads, -) - -data class Downloads( - @SerializedName("1.8.9") val version189: String, - @SerializedName("1.20.5") val version1205: String, - val bedrock: String, -) - -class PlayerAdminUI( - private val game: Game, - private val managedPlayer: Player, -) : InventoryHolder { - private val inventory = Bukkit.createInventory(this, 9, Component.text("Admin UI")) - - init { - val openVoiceItem = ItemStack.of(Material.NOTE_BLOCK) - openVoiceItem.editMeta { meta -> - meta.displayName(Component.text("Open Voice Chat").decoration(TextDecoration.ITALIC, false)) - meta.lore( - listOf( - Component - .text("Moves the selected player into the Discord stage") - .color(NamedTextColor.GRAY) - .decoration(TextDecoration.ITALIC, false), - ), - ) - } - val playerDataItem = ItemStack.of(Material.PAPER) - playerDataItem.editMeta { meta -> - val playerData = game.playerData(managedPlayer) - if (playerData == null) { - meta.displayName(Component.text("No player data").decoration(TextDecoration.ITALIC, false)) - return@editMeta - } - - meta.displayName(Component.text("Player Data").decoration(TextDecoration.ITALIC, false)) - meta.lore( - listOf( - Component - .text("Score: ${playerData.score}") - .color(NamedTextColor.GRAY) - .decoration(TextDecoration.ITALIC, false), - ), - ) - } - inventory.apply { - setItem(0, openVoiceItem) - setItem(1, playerDataItem) - } - } - - override fun getInventory(): Inventory = inventory - - fun onInventoryClick(event: InventoryClickEvent) { - if (event.rawSlot == 0) { - // send a GET request to https://bedless.mester.info/api/packdata (expect JSON) - val request = - Request - .Builder() - .url("https://bedless.mester.info/api/packdata") - .build() - - PartyGames.httpClient - .newCall(request) - .execute() - .use { response -> - if (!response.isSuccessful) { - event.whoClicked.sendMessage( - Component.text("Failed to fetch pack data", NamedTextColor.RED), - ) - return - } - val packData = response.body?.string() - if (packData == null) { - event.whoClicked.sendMessage( - Component.text("Failed to fetch pack data", NamedTextColor.RED), - ) - return - } - val gson = Gson() - val packInfo = gson.fromJson(packData, PackInfo::class.java) - - event.whoClicked.sendMessage( - Component.text( - packInfo.packs.find { it.id == "60k" }?.friendlyName ?: "Pack not found", - NamedTextColor.RED, - ), - ) - } - } - } -} diff --git a/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt b/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt deleted file mode 100644 index c32a45f..0000000 --- a/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt +++ /dev/null @@ -1,146 +0,0 @@ -package info.mester.network.partygames.game - -import info.mester.network.partygames.PartyGames -import net.kyori.adventure.audience.Audience -import net.kyori.adventure.text.minimessage.MiniMessage -import org.bukkit.World -import org.bukkit.entity.Player -import java.util.UUID -import kotlin.reflect.KClass - -enum class GameType( - val minigames: List>, - val displayName: String, -) { - HEALTH_SHOP(listOf(HealthShopMinigame::class), "Health Shop"), - SPEED_BUILDERS(listOf(SpeedBuildersMinigame::class), "Speed Builders"), - GARDENING(listOf(GardeningMinigame::class), "Gardening"), - FAMILY_NIGHT( - listOf( - HealthShopMinigame::class, - SpeedBuildersMinigame::class, - GardeningMinigame::class, - DamageDealer::class, - ), - "Family Night", - ), - SNIFFER_HUNT( - listOf( - SnifferHuntMinigame::class, - ), - "Sniffer Hunt", - ), - DAMAGE_DEALER( - listOf( - DamageDealer::class, - ), - "Damage Dealer", - ), -} - -private val mm = MiniMessage.miniMessage() - -class GameManager( - private val plugin: PartyGames, -) { - private val queues = mutableMapOf() - private val games = mutableMapOf() - - private fun createQueue( - type: GameType, - maxPlayers: Int = 8, - ): Queue { - val queue = Queue(type, maxPlayers, this) - queues[queue.id] = queue - return queue - } - - private fun getQueueForPlayers( - type: GameType, - players: List, - ): Queue { - // either return the first queue that can still fit the players, or create a new queue - val queue = queues.values.firstOrNull { it.type == type && it.maxPlayers - it.playerCount >= players.size } - if (queue != null) { - return queue - } - return createQueue(type) - } - - fun removeQueue(id: UUID) { - queues.remove(id) - } - - fun joinQueue( - type: GameType, - players: List, - ) { -// // check for minimum version -// val inCompatiblePlayers = players.filter { plugin.viaAPI.getPlayerVersion(it.uniqueId) < type.minVersion } -// if (inCompatiblePlayers.isNotEmpty()) { -// val compatibleAudience = Audience.audience(players.filter { it !in inCompatiblePlayers }) -// val incompatibleAudience = Audience.audience(inCompatiblePlayers) -// compatibleAudience.sendMessage( -// mm.deserialize( -// "You are trying to join a game that is not compatible with your Minecraft version! Please play using the latest version of Minecraft if you want to guarantee full compatibility.", -// ), -// ) -// incompatibleAudience.sendMessage( -// mm.deserialize( -// "The following players are using an incompatible version of Minecraft and cannot join this game:", -// ), -// ) -// incompatibleAudience.sendMessage( -// mm.deserialize( -// inCompatiblePlayers -// .map { "${it.name}" } -// .joinToString { ",You are already in a game! You cannot join another game!", - ), - ) - return - } - // remove players from queues that already have them - for (player in players) { - removePlayerFromQueue(player) - } - val queue = getQueueForPlayers(type, players) - queue.addPlayers(players) - } - - fun getQueueOf(player: Player) = queues.values.firstOrNull { it.hasPlayer(player) } - - fun getGameOf(player: Player) = games.values.firstOrNull { it.hasPlayer(player) } - - fun getGameByWorld(world: World) = games.values.firstOrNull { it.worldName == world.name } - - fun shutdown() { - games.values.forEach { it.terminate() } - } - - fun removePlayerFromQueue(player: Player) { - getQueueOf(player)?.removePlayer(player) - } - - fun startGame(queue: Queue) { - queues.remove(queue.id) - val players = queue.getPlayers() - val game = Game(plugin, queue.type, players) - games[game.id] = game - } - - fun getGames(): Array = games.values.toTypedArray() - - fun removeGame(game: Game) { - games.remove(game.id) - } -} diff --git a/src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt b/src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt deleted file mode 100644 index 40632c1..0000000 --- a/src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt +++ /dev/null @@ -1,86 +0,0 @@ -package info.mester.network.partygames.game - -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import org.bukkit.Bukkit -import org.bukkit.entity.Player -import org.bukkit.event.entity.PlayerDeathEvent -import org.bukkit.scheduler.BukkitTask -import java.util.function.Consumer -import kotlin.math.floor - -class RunawayMinigame( - game: Game, -) : Minigame(game, "runaway") { - override fun start() { - super.start() - // start a 30-second countdown for the minigame - startCountdown(30000) { - end() - } - // start a timer that is constantly updating every player's actionbar with the distance from the start position - Bukkit.getScheduler().runTaskTimer( - plugin, - object : Consumer { - override fun accept(t: BukkitTask) { - if (!running) { - t.cancel() - return - } - val distances = sortedDistances - - for (player in game.onlinePlayers) { - val distance = distances.find { it.first.uniqueId == player.uniqueId }!!.second - // show message in player's actionbar - player.sendActionBar(Component.text("Distance from start: $distance blocks")) - } - } - }, - 0, - 2, - ) - } - - val sortedDistances: List> - get() { - // calculate the distance between all players and the start position as HashMap - return game - .onlinePlayers - .associateWith { String.format("%.2f", startPos.distance(it.location)).toDouble() } - // sort by descending distance - .toList() - .sortedByDescending { it.second } - } - - override fun finish() { - val distances = sortedDistances - // scoring system: +1 point for every 10 blocks away, +5 points for being #1 in the list, +3 points for being #2 or #3 +1 points for being in the top 10 - for (player in game.onlinePlayers) { - val distanceScore = floor(distances.find { it.first.uniqueId == player.uniqueId }!!.second / 10).toInt() - // get the position of the player in the list - val position = distances.indexOfFirst { it.first.uniqueId == player.uniqueId } - val leaderboardScore = - when (position) { - 0 -> 5 - 1, 2 -> 3 - in 3..9 -> 1 - else -> 0 - } - - game.addScore(player, distanceScore, "Distance to start") - if (leaderboardScore != 0) { - game.addScore(player, leaderboardScore, "Leaderboard position") - } - } - } - - override fun handlePlayerDeath(event: PlayerDeathEvent) { - event.isCancelled = true - super.handlePlayerDeath(event) - } - - override val name: Component - get() = Component.text("Runaway", NamedTextColor.AQUA) - override val description: Component - get() = Component.text("Run as far away as possible in 30 seconds!", NamedTextColor.AQUA) -} diff --git a/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopItem.kt b/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopItem.kt deleted file mode 100644 index b56f0e2..0000000 --- a/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopItem.kt +++ /dev/null @@ -1,64 +0,0 @@ -package info.mester.network.partygames.game.healthshop - -import info.mester.network.partygames.util.createBasicItem -import org.bukkit.Material -import org.bukkit.configuration.ConfigurationSection -import org.bukkit.inventory.ItemStack - -class HealthShopItem( - val item: ItemStack, - val price: Int, - val slot: Int, - val key: String, - val category: String, - val amount: Int = 1, -) { - companion object { - fun loadFromConfig( - section: ConfigurationSection, - key: String, - ): HealthShopItem { - val material = Material.matchMaterial(section.getString("id")!!) - val amount = section.getInt("amount", 1) - val lore = - ( - section.getStringList("lore") + - listOf( - "", - "Cost: ${String.format("%.1f", section.getInt("price") / 2.0)} ♥", - ) - ).toTypedArray() - val item = - createBasicItem(material ?: Material.BARRIER, section.getString("name") ?: "Unknown", amount, *lore) - item.editMeta { meta -> - meta.setEnchantmentGlintOverride(false) - HealthShopUI.applyGenericItemMeta(meta) - } - // apply healing potion to item - if (key == "splash_healing_i" || key == "splash_healing_ii") { - HealthShopUI.setHealthPotion(item, key == "splash_healing_ii") - } - // apply regeneration potion to item - if (key == "regen_potion") { - HealthShopUI.setRegenPotion(item, false) - } - // apply speed potion to item - if (key == "speed_potion") { - HealthShopUI.setSpeedPotion(item, false) - } - // apply jump potion to item - if (key == "jump_potion") { - HealthShopUI.setJumpPotion(item, false) - } - - return HealthShopItem( - item, - section.getInt("price"), - section.getInt("slot"), - key, - section.getString("category") ?: "none", - amount, - ) - } - } -} diff --git a/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopUI.kt b/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopUI.kt deleted file mode 100644 index 727ec49..0000000 --- a/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopUI.kt +++ /dev/null @@ -1,487 +0,0 @@ -package info.mester.network.partygames.game.healthshop - -import info.mester.network.partygames.PartyGames -import info.mester.network.partygames.game.ShopFailedException -import info.mester.network.partygames.toRomanNumeral -import net.kyori.adventure.key.Key -import net.kyori.adventure.sound.Sound -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.format.TextDecoration -import net.kyori.adventure.text.minimessage.MiniMessage -import org.bukkit.Bukkit -import org.bukkit.Color -import org.bukkit.Material -import org.bukkit.NamespacedKey -import org.bukkit.enchantments.Enchantment -import org.bukkit.entity.Player -import org.bukkit.event.inventory.InventoryClickEvent -import org.bukkit.inventory.EquipmentSlot -import org.bukkit.inventory.Inventory -import org.bukkit.inventory.InventoryHolder -import org.bukkit.inventory.ItemFlag -import org.bukkit.inventory.ItemStack -import org.bukkit.inventory.meta.ItemMeta -import org.bukkit.inventory.meta.PotionMeta -import org.bukkit.persistence.PersistentDataType -import org.bukkit.potion.PotionEffect -import org.bukkit.potion.PotionEffectType -import org.bukkit.potion.PotionType -import java.util.UUID - -class HealthShopUI( - playerUUID: UUID, - private val items: List, - private var money: Double, -) : InventoryHolder { - private val inventory = Bukkit.createInventory(this, 5 * 9, Component.text("Health Shop")) - private val purchasedItems: MutableList = mutableListOf() - private val player = Bukkit.getPlayer(playerUUID)!! - - companion object { - private val maxArrows: MutableMap = mutableMapOf() - - fun maxArrows(uuid: UUID): Int = maxArrows[uuid] ?: 0 - - fun setHealthPotion( - item: ItemStack, - strong: Boolean, - ) { - item.editMeta { meta -> - val potionMeta = meta as PotionMeta - potionMeta.basePotionType = if (strong) PotionType.STRONG_HEALING else PotionType.HEALING - applyGenericItemMeta(meta) - } - } - - private fun setCustomPotion( - item: ItemStack, - potionEffect: PotionEffect, - color: Color, - potionName: String?, - ) { - item.editMeta { meta -> - val potionMeta = meta as PotionMeta - potionMeta.addCustomEffect(potionEffect, true) - potionMeta.color = color - val duration = potionEffect.duration / 20 - val minutes = duration / 60 - val seconds = String.format("%02d", duration % 60) - if (potionName != null) { - val name = - MiniMessage - .miniMessage() - .deserialize( - "$potionName ${(potionEffect.amplifier + 1).toRomanNumeral()} ($minutes:$seconds)", - ) - meta.displayName(name) - } - applyGenericItemMeta(meta) - } - } - - fun setRegenPotion( - item: ItemStack, - withName: Boolean = true, - ) = setCustomPotion( - item, - PotionEffect(PotionEffectType.REGENERATION, 5 * 20, 4, false), - Color.fromRGB(205, 92, 171), - if (withName) "Regeneration" else null, - ) - - fun setSpeedPotion( - item: ItemStack, - withName: Boolean = true, - ) = setCustomPotion( - item, - PotionEffect(PotionEffectType.SPEED, 15 * 20, 1, false), - Color.fromRGB(51, 235, 255), - if (withName) "Speed" else null, - ) - - fun setJumpPotion( - item: ItemStack, - withName: Boolean = true, - ) = setCustomPotion( - item, - PotionEffect(PotionEffectType.JUMP_BOOST, 20 * 20, 3, false), - Color.fromRGB(253, 255, 132), - if (withName) "Jump Boost" else null, - ) - - fun applyGenericItemMeta(itemMeta: ItemMeta) { - itemMeta.apply { - isUnbreakable = true - addItemFlags(ItemFlag.HIDE_UNBREAKABLE) - addItemFlags(ItemFlag.HIDE_ADDITIONAL_TOOLTIP) - addItemFlags(ItemFlag.HIDE_ATTRIBUTES) - } - } - } - - init { - // init maxarrows to 0 - maxArrows[playerUUID] = 0 - for (item in items) { - inventory.setItem(item.slot, item.item) - } - } - - override fun getInventory(): Inventory = inventory - - fun onInventoryClick(event: InventoryClickEvent) { - val slot = event.slot - val index = items.indexOfFirst { it.slot == slot } - if (index == -1) { - return - } - val shopItem = items[index] - val item = inventory.getItem(slot)!! - // toggle purchased state - if (purchasedItems.contains(shopItem)) { - removeItem(shopItem, item) - } else { - try { - addItem(shopItem, item) - } catch (e: ShopFailedException) { - val message = - when (e.message) { - "no_healh" -> "You do not have enough hearts to purchase this item!" - "no_bow" -> "You must first buy a bow before purchasing this item!" - else -> "An error occurred while trying to purchase this item!" - } - event.whoClicked.sendMessage( - Component.text( - message, - NamedTextColor.RED, - ), - ) - player.playSound( - Sound.sound(Key.key("entity.villager.no"), Sound.Source.MASTER, 1.0f, 1.0f), - Sound.Emitter.self(), - ) - } - } - player.sendMessage( - MiniMessage - .miniMessage() - .deserialize("You have ${String.format("%.1f", player.health / 2.0)} ♥ left!"), - ) - } - - private fun removeItem( - shopItem: HealthShopItem, - inventoryItem: ItemStack, - ) { - purchasedItems.remove(shopItem) - money += shopItem.price - - inventoryItem.editMeta { meta -> - // remove the enchantment glint from the item in the ui - meta.setEnchantmentGlintOverride(false) - // remove underlined and bold from name - val decorations = - mapOf( - TextDecoration.UNDERLINED to TextDecoration.State.FALSE, - TextDecoration.BOLD to TextDecoration.State.FALSE, - ) - meta.displayName(meta.displayName()!!.decorations(decorations)) - } - // special case: if we remove a bow, remove all arrows - if (shopItem.key == "bow") { - val arrowItem = purchasedItems.firstOrNull { it.category == "arrow" } - if (arrowItem != null) { - removeItem(arrowItem, inventory.getItem(arrowItem.slot)!!) - } - } - - player.health = money - } - - private fun addItem( - shopItem: HealthShopItem, - inventoryItem: ItemStack, - ) { - val sameCategory = purchasedItems.filter { it.category != "none" && it.category == shopItem.category } - // calculate how much money we'd have if we removed all the items in the same category - val moneyToAdd = sameCategory.sumOf { it.price } - // check if we have enough money - if ((money + moneyToAdd) <= shopItem.price) { - throw ShopFailedException("no_health") - } - // check if we're trying to buy an arrow - if (shopItem.category == "arrow") { - // check if we have a bow - if (!purchasedItems.any { it.key == "bow" }) { - throw ShopFailedException("no_bow") - } - } - purchasedItems.add(shopItem) - money -= shopItem.price - // play experience orb pickup sound - player.playSound( - Sound.sound(Key.key("entity.experience_orb.pickup"), Sound.Source.MASTER, 1.0f, 1.0f), - Sound.Emitter.self(), - ) - // remove all the items in the same category - sameCategory.forEach { removeItem(it, inventory.getItem(it.slot)!!) } - - inventoryItem.editMeta { meta -> - // add an enchantment glint to the item in the ui - meta.setEnchantmentGlintOverride(true) - // make name underlined and bold - val decorations = - mapOf( - TextDecoration.UNDERLINED to TextDecoration.State.TRUE, - TextDecoration.BOLD to TextDecoration.State.TRUE, - ) - meta.displayName(meta.displayName()!!.decorations(decorations)) - } - - player.health = money - } - - private fun addArmor( - player: Player, - armor: ArmorType, - ) { - // this ugly shit creates a list of armor items based on the armor type - // order: helmet, chestplate, leggings, boots - val armorItems = - when (armor) { - ArmorType.LEATHER -> - listOf( - ItemStack.of(Material.LEATHER_HELMET), - ItemStack.of(Material.LEATHER_CHESTPLATE), - ItemStack.of(Material.LEATHER_LEGGINGS), - ItemStack.of(Material.LEATHER_BOOTS), - ) - - ArmorType.CHAINMAIL -> - listOf( - ItemStack.of(Material.CHAINMAIL_HELMET), - ItemStack.of(Material.CHAINMAIL_CHESTPLATE), - ItemStack.of(Material.CHAINMAIL_LEGGINGS), - ItemStack.of(Material.CHAINMAIL_BOOTS), - ) - - ArmorType.IRON -> - listOf( - ItemStack.of(Material.IRON_HELMET), - ItemStack.of(Material.IRON_CHESTPLATE), - ItemStack.of(Material.IRON_LEGGINGS), - ItemStack.of(Material.CHAINMAIL_BOOTS), - ) - - ArmorType.DIAMOND -> - listOf( - ItemStack.of(Material.DIAMOND_HELMET), - ItemStack.of(Material.DIAMOND_CHESTPLATE), - ItemStack.of(Material.IRON_LEGGINGS), - ItemStack.of(Material.IRON_BOOTS), - ) - - ArmorType.NETHERITTE -> - listOf( - ItemStack.of(Material.NETHERITE_HELMET), - ItemStack.of(Material.NETHERITE_CHESTPLATE), - ItemStack.of(Material.DIAMOND_LEGGINGS), - ItemStack.of(Material.DIAMOND_BOOTS), - ) - } - armorItems.forEach { armorItem -> - armorItem.editMeta { meta -> - applyGenericItemMeta(meta) - if (purchasedItems.any { it.key == "protection_i" }) { - meta.addEnchant(Enchantment.PROTECTION, 1, true) - } - if (purchasedItems.any { it.key == "protection_ii" }) { - meta.addEnchant(Enchantment.PROTECTION, 2, true) - } - if (purchasedItems.any { it.key == "thorns" }) { - meta.addEnchant(Enchantment.THORNS, 2, true) - } - } - } - - player.inventory.setItem(EquipmentSlot.HEAD, armorItems[0]) - player.inventory.setItem(EquipmentSlot.CHEST, armorItems[1]) - player.inventory.setItem(EquipmentSlot.LEGS, armorItems[2]) - player.inventory.setItem(EquipmentSlot.FEET, armorItems[3]) - } - - fun giveItems() { - // process sword - val addSword = { material: Material -> - val sword = ItemStack.of(material) - sword.editMeta { meta -> - if (purchasedItems.any { it.key == "fire_aspect" }) { - meta.addEnchant(Enchantment.FIRE_ASPECT, 1, true) - } - purchasedItems.firstOrNull { it.key.startsWith("sharpness_") }.let { sharpnessItem -> - if (sharpnessItem != null) { - val sharpness = sharpnessItem.key.substringAfter("sharpness_").toInt() - meta.addEnchant(Enchantment.SHARPNESS, sharpness, true) - } - } - applyGenericItemMeta(meta) - } - player.inventory.addItem(sword) - } - kotlin - .runCatching { - purchasedItems.first { it.category == "sword" }.item.type - }.onSuccess { material -> - addSword(material) - }.onFailure { - addSword(Material.WOODEN_SWORD) - } - // process knockback stick - if (purchasedItems.any { it.key == "knockback_stick" }) { - val stick = ItemStack.of(Material.STICK) - stick.editMeta { meta -> - applyGenericItemMeta(meta) - meta.addEnchant(Enchantment.KNOCKBACK, 2, true) - } - player.inventory.addItem(stick) - } - // process shield - if (purchasedItems.any { it.key == "shield" }) { - val shield = ItemStack.of(Material.SHIELD) - shield.editMeta { meta -> - applyGenericItemMeta(meta) - } - player.inventory.setItem(EquipmentSlot.OFF_HAND, shield) - } - // process golden apples - purchasedItems.filter { it.category == "gap" }.forEach { item -> - val apple = ItemStack.of(Material.GOLDEN_APPLE, item.amount) - if (item.key == "golden_apple_inf") { - apple.editMeta { meta -> - // add a fake enchantment to make it look like an enchanted golden apple - // the enchantment is also used to determine if the item is an infinite golden apple - meta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true) - meta.addItemFlags(ItemFlag.HIDE_ENCHANTS) - } - } - player.inventory.addItem(apple) - } - // process regeneration potion - purchasedItems.firstOrNull { it.key == "regen_potion" }?.let { shopItem -> - val potion = ItemStack.of(Material.POTION) - setRegenPotion(potion) - for (i in 1..shopItem.amount) { - player.inventory.addItem(potion) - } - } - // process speed potion - purchasedItems.firstOrNull { it.key == "speed_potion" }?.let { shopItem -> - val potion = ItemStack.of(Material.POTION) - setSpeedPotion(potion) - for (i in 1..shopItem.amount) { - player.inventory.addItem(potion) - } - } - // process jump potion - purchasedItems.firstOrNull { it.key == "jump_potion" }?.let { shopItem -> - val potion = ItemStack.of(Material.POTION) - setJumpPotion(potion) - for (i in 1..shopItem.amount) { - player.inventory.addItem(potion) - } - } - // process healing potions - for (purchasedPotion in purchasedItems.filter { it.key == "splash_healing_i" || it.key == "splash_healing_ii" }) { - val potion = ItemStack.of(Material.SPLASH_POTION, purchasedPotion.amount) - setHealthPotion(potion, purchasedPotion.key == "splash_healing_ii") - player.inventory.addItem(potion) - } - // process armor - kotlin - .runCatching { - purchasedItems.first { it.category == "armor" } - }.onSuccess { shopItem -> - when (shopItem.key) { - "chainmail_armor" -> addArmor(player, ArmorType.CHAINMAIL) - "iron_armor" -> addArmor(player, ArmorType.IRON) - "diamond_armor" -> addArmor(player, ArmorType.DIAMOND) - "netherite_armor" -> addArmor(player, ArmorType.NETHERITTE) - } - }.onFailure { - addArmor(player, ArmorType.LEATHER) - } - // process bow - if (purchasedItems.any { it.key == "bow" }) { - val bow = ItemStack.of(Material.BOW) - bow.editMeta { meta -> - applyGenericItemMeta(meta) - if (purchasedItems.any { it.key == "flame" }) { - meta.addEnchant(Enchantment.FLAME, 1, true) - } - if (purchasedItems.any { it.key == "power_i" }) { - meta.addEnchant(Enchantment.POWER, 1, true) - } - if (purchasedItems.any { it.key == "power_ii" }) { - meta.addEnchant(Enchantment.POWER, 2, true) - } - if (purchasedItems.any { it.key == "punch_i" }) { - meta.addEnchant(Enchantment.PUNCH, 1, true) - } - if (purchasedItems.any { it.key == "punch_ii" }) { - meta.addEnchant(Enchantment.PUNCH, 2, true) - } - } - player.inventory.addItem(bow) - // an arrow is included with the bow - maxArrows[player.uniqueId] = 1 - } - // process arrows - kotlin - .runCatching { - purchasedItems.first { it.category == "arrow" } - }.onSuccess { shopItem -> - maxArrows[player.uniqueId] = maxArrows[player.uniqueId]!! + shopItem.amount - } - // process tracker - if (purchasedItems.any { it.key == "tracker" }) { - val tracker = ItemStack.of(Material.COMPASS) - tracker.editMeta { meta -> - meta.setEnchantmentGlintOverride(false) - } - player.inventory.addItem(tracker) - } - // process steal perk - if (purchasedItems.any { it.key == "steal_perk" }) { - player.persistentDataContainer.set( - NamespacedKey(PartyGames.plugin, "steal_perk"), - PersistentDataType.BOOLEAN, - true, - ) - } - // process heal perk - if (purchasedItems.any { it.key == "heal_perk" }) { - player.persistentDataContainer.set( - NamespacedKey(PartyGames.plugin, "heal_perk"), - PersistentDataType.BOOLEAN, - true, - ) - } - // process double jump - if (purchasedItems.any { it.key == "double_jump" }) { - player.persistentDataContainer.set( - NamespacedKey(PartyGames.plugin, "double_jump"), - PersistentDataType.BOOLEAN, - true, - ) - } - // process flint and steel - if (purchasedItems.any { it.key == "flint_and_steel" }) { - player.inventory.addItem(ItemStack.of(Material.FLINT_AND_STEEL, 1)) - } - // process oak planks - if (purchasedItems.any { it.key == "oak_planks" }) { - player.inventory.addItem(ItemStack.of(Material.OAK_PLANKS, 24)) - } - } -} diff --git a/src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt b/src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt deleted file mode 100644 index 1d0b9ea..0000000 --- a/src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt +++ /dev/null @@ -1,19 +0,0 @@ -package info.mester.network.partygames.util - -import net.kyori.adventure.text.minimessage.MiniMessage -import org.bukkit.Material -import org.bukkit.inventory.ItemStack - -fun createBasicItem( - material: Material, - name: String, - count: Int = 1, - vararg lore: String, -): ItemStack { - val item = ItemStack.of(material, count) - item.editMeta { meta -> - meta.displayName(MiniMessage.miniMessage().deserialize("$name")) - meta.lore(lore.map { MiniMessage.miniMessage().deserialize("$it") }) - } - return item -} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml deleted file mode 100644 index 7f44f87..0000000 --- a/src/main/resources/config.yml +++ /dev/null @@ -1,51 +0,0 @@ -locations: - minigames: - runaway: - worlds: - - mg-runaway - x: 0.5 - y: 63.0 - z: 0.5 - healthshop: - worlds: - - mg-healthshop - - mg-healthshop2 - x: 0.5 - y: 63.0 - z: 0.5 - speedbuilders: - worlds: - - mg-speedbuilders - x: 0.5 - y: 60.0 - z: 0.5 - gardening: - worlds: - - mg-gardening - x: 0.5 - y: 65.0 - z: 0.5 - snifferhunt: - worlds: - - mg-snifferhunt - x: 0.5 - y: 65.0 - z: 0.5 - damagedealer: - worlds: - - mg-damagedealer - x: 0.5 - y: 62.0 - z: 0.5 -steve-skin: - value: ewogICJ0aW1lc3RhbXAiIDogMTcyMTU3OTU0NDE2NCwKICAicHJvZmlsZUlkIiA6ICIwNDA0MjM4NGNmNWU0ZjU4YTEyODZhODdlZGU0NjFiNCIsCiAgInByb2ZpbGVOYW1lIiA6ICJCZWRsZXNzTm9vYiIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9kOWE1OWVhYTgyYTE4OWI0ZWU3NGY4ODExYWE5NmEwM2EwMzJhMzY0ZjkwNWRlNzFlMTY0NTZiYmZjYWY2ZWM5IgogICAgfSwKICAgICJDQVBFIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9jZDlkODJhYjE3ZmQ5MjAyMmRiZDRhODZjZGU0YzM4MmE3NTQwZTExN2ZhZTdiOWEyODUzNjU4NTA1YTgwNjI1IgogICAgfQogIH0KfQ== - signature: YXLlxY1qA7zONTGDDqreFYtLJry7IIDg2xFyQx8mL9YOGtT0x6dD3KCq/L0qpTsoH2z4G5JnY1LwQpLzZ4+o4pMUsuacDbRaNooygloSudUO9BLqtbA8jj0aQvXcNcSJ7+9wkDtEdeHZaiRN8uRRx/zWBf0hEKNKcCyyHvUMNjDhJ97McmTg3quTMxeW6WZRTIDPreQ4IttRy4JeyrA+YesqBmII/94G4f6/AGJsJm29U4GnHtgSiWKrw5yHc1CACA+fEUGj4Amv7+w8N9XMXQguySY9OivU0a9eKZQ7fxGtGSfzdSt4FSj5AtvG31MLhmmWwQLlI5i6xkQsjbtiRtdGoITjs/kTzxFxt8KfuJPXSMEpOpzkuBZmfiwTnD049c3npsZyXoqgYERcDSj8AbEDNlaoTpE9+zsdEVjEKzIreRYffK/wLcg8pwUcCf5iSWQ5WE6ML4OuzDT2+Agj2Dw97xIc2ErYP5hmtYFv6pFIZIFFppKBlrrI0f29uIXYSZ2wA48fnZzhe3dpw/GBRGBJQPctIqK/E//FOE0CtGXHMcBSY5WQR0YFHSYA6iTH9KqaVej/sT1JF1ImVi2ue92iODtPSsJerlFonstW29J3mATUO5kg1TgYsghASdjjI9+5r6EbfVF53C5AsvdKHN4Wg8QABbzkseLGXMOR4Js= -save-interval: 10 -spawn-location: - ==: org.bukkit.Location - world: world - x: 78.5 - y: 93.0 - z: 40.5 - yaw: 90.0 - pitch: 0.0 \ No newline at end of file diff --git a/src/main/resources/health-shop.yml b/src/main/resources/health-shop.yml deleted file mode 100644 index afb849d..0000000 --- a/src/main/resources/health-shop.yml +++ /dev/null @@ -1,416 +0,0 @@ -items: - stone_sword: - id: stone_sword - name: Stone Sword - lore: - - A basic, but trustworthy stone sword. - - 9 out of 10 cavemen recommend it. - price: 8 - slot: 0 - category: sword - iron_sword: - id: iron_sword - name: Iron Sword - lore: - - A sharp and durable iron sword. - - 10 out of 10 blacksmiths recommend it. - price: 12 - slot: 1 - category: sword - diamond_sword: - id: diamond_sword - name: Diamond Sword - lore: - - A powerful and enduring diamond sword. - - A favorite among treasure hunters. - price: 16 - slot: 2 - category: sword - netherite_sword: - id: netherite_sword - name: Netherite Sword - lore: - - The strongest sword in existence, imbued with ancient power. - - Only the bravest wield this mighty blade. - price: 20 - slot: 3 - category: sword - shield: - id: shield - name: Shield - lore: - - A basic shield, good for blocking attacks. - - Has a 1.5 second cooldown after use. - price: 3 - slot: 4 - knockback_stick: - id: stick - name: Knockback Stick - lore: - - A stick with Knockback II. - - Great if you want to be annoying. - price: 5 - slot: 5 - fire_aspect: - id: enchanted_book - name: Fire Aspect - lore: - - Let the world burn! - price: 10 - slot: 6 - sharpness_3: - id: enchanted_book - name: Sharpness III - lore: - - Increases your damage by 1♥ - - May cause a bit of pain. - price: 12 - slot: 7 - category: sharpness - amount: 1 - sharpness_5: - id: enchanted_book - name: Sharpness V - lore: - - Increases your damage by 1.5♥ - - OUCH! - price: 18 - slot: 8 - category: sharpness - amount: 1 - golden_apple_1: - id: golden_apple - name: Golden Apple - lore: - - A single golden apple. - - It's a delicious treat! - price: 2 - slot: 9 - category: gap - golden_apple_2: - id: golden_apple - name: Golden Apple - lore: - - Two golden apples. - - Now we're talking! - price: 4 - slot: 10 - category: gap - amount: 2 - golden_apple_3: - id: golden_apple - name: Golden Apple - lore: - - Three golden apples. - - Enough regen for the whole family! - price: 6 - slot: 11 - category: gap - amount: 3 - golden_apple_inf: - id: enchanted_golden_apple - name: Infinite Golden Apple - lore: - - Infinite golden apples. - - Regenerates every 10 seconds! - price: 14 - slot: 12 - category: gap - speed_potion: - id: potion - name: 3x Potion of Speed II - lore: - - Speeds you up for 15 seconds. - - Perfect for catching goons. - price: 6 - slot: 13 - amount: 3 - jump_potion: - id: potion - name: 3x Potion of Jump Boost IV - lore: - - Turns you into a rabbit for 20 seconds. - - Boing-boing-boing! - price: 6 - slot: 14 - amount: 3 - regen_potion: - id: potion - name: 2x Potion of Regeneration V - lore: - - Regenerates 3 per second. - - Liquid Enchanted Golden Apples. - price: 12 - slot: 15 - amount: 2 - splash_healing_i: - id: splash_potion - name: 5x Splash Potion of Healing I - lore: - - Heals you for 2 ♥ - - Applies instantly. - price: 4 - slot: 16 - amount: 5 - splash_healing_ii: - id: splash_potion - name: 5x Splash Potion of Healing II - lore: - - Heals you for 4 ♥ - - Applies instantly. - price: 8 - slot: 17 - amount: 5 - chainmail_armor: - id: chainmail_chestplate - name: Chainmail Armor - lore: - - A basic chainmail armor. - - Good for blocking attacks. - price: 8 - slot: 18 - category: armor - iron_armor: - id: iron_chestplate - name: Iron Armor - lore: - - A basic iron armor. - - Good for blocking attacks. - price: 12 - slot: 19 - category: armor - diamond_armor: - id: diamond_chestplate - name: Diamond Armor - lore: - - A basic diamond armor. - - Good for blocking attacks. - price: 24 - slot: 20 - category: armor - netherite_armor: - id: netherite_chestplate - name: Netherite Armor - lore: - - The strongest armor in existence, imbued with ancient power. - - Only the bravest wield this mighty armor. - price: 36 - slot: 21 - category: armor - thorns: - id: enchanted_book - name: Thorns - lore: - - Damages anyone who attacks you. - - Maybe don't use it on yourself. - price: 14 - slot: 24 - protection_i: - id: enchanted_book - name: Protection I - lore: - - Gives you a 16% damage reduction. - price: 10 - slot: 25 - category: protection - protection_ii: - id: enchanted_book - name: Protection II - lore: - - Gives you a 32% damage reduction. - price: 20 - slot: 26 - category: protection - bow: - id: bow - name: Bow - lore: - - A bow that shoots arrows. - - Comes with a regenerating arrow. - price: 8 - slot: 27 - arrow_1: - id: arrow - name: Arrow - lore: - - A basic arrow. - - Regenerates after 3 seconds. - price: 3 - slot: 28 - category: arrow - arrow_2: - id: arrow - name: Arrow - lore: - - 2 arrows. - - Regenerates after 3 seconds. - price: 6 - slot: 29 - category: arrow - amount: 2 - arrow_3: - id: arrow - name: Arrow - lore: - - 3 arrows. - - Regenerates after 3 seconds. - price: 9 - slot: 30 - category: arrow - amount: 3 - flame: - id: enchanted_book - name: Flame - lore: - - Didn't they use flaming arrows in the past? - price: 10 - slot: 31 - power_i: - id: enchanted_book - name: Power I - lore: - - Increases your damage by 50%, kinda hurts. - price: 8 - slot: 32 - category: power - power_ii: - id: enchanted_book - name: Power II - lore: - - Increases your damage by 75%. - - Use it well, and you can one-shot someone. - price: 12 - slot: 33 - category: power - punch_i: - id: enchanted_book - name: Punch I - lore: - - Send them flying! - price: 6 - slot: 34 - category: punch - punch_ii: - id: enchanted_book - name: Punch II - lore: - - Can't touch this! - price: 10 - slot: 35 - category: punch - tracker: - id: compass - name: Tracker - lore: - - Right click to point to the nearest player. - - Has a 5-second cooldown and alerts the tracked player. - price: 6 - slot: 36 - steal_perk: - id: bundle - name: Steal Perk - lore: - - You will get every item from the players you kill. - - The perk can only be used once. Very high risk, high reward. - price: 20 - slot: 37 - category: perk - heal_perk: - id: red_dye - name: Heal Perk - lore: - - You will heal to full health when you kill a player. - - The perk is kept after use. - price: 7 - slot: 38 - category: perk - double_jump: - id: rabbit_foot - name: Double Jump - lore: - - Gives you the ability to jump twice. - - The perk is kept after use and has a 3 second cooldown. - price: 7 - slot: 39 - category: perk - flint_and_steel: - id: flint_and_steel - name: Flint and Steel - lore: - - A flint and steel, the same thing that ooga-booga used. - - Great if you want to get arrested for arson. - price: 5 - slot: 43 - amount: 1 - oak_planks: - id: oak_planks - name: Oak Planks - lore: - - A basic oak plank. - - Good for building. - price: 4 - slot: 44 - amount: 32 -spawn-locations: - 0: - - x: 52.5 - y: 64.0 - z: -51.5 - - x: 13.5 - y: 69.0 - z: -58.5 - - x: -48.5 - y: 65.0 - z: -43.5 - - x: -48.5 - y: 64.0 - z: 22.5 - - x: -26.5 - y: 77.0 - z: 54.5 - - x: 49.5 - y: 77.0 - z: 45.5 - - x: 48.5 - y: 67.0 - z: 3.5 - - x: -0.5 - y: 64.0 - z: -0.5 - 1: - - x: 36.5 - y: 56.0 - z: 36.5 - - x: 28.5 - y: 58.0 - z: 28.5 - - x: -34.5 - y: 56.0 - z: 36.5 - - x: -27.5 - y: 58.0 - z: 29.5 - - x: -34.5 - y: 56.0 - z: -35.5 - - x: -27.5 - y: 58.0 - z: -28.5 - - x: 35.5 - y: 56.0 - z: -35.5 - - x: 28.5 - y: 58.0 - z: -28.5 -supply-drops: - - key: golden_apple_1 - weight: 60 - - key: golden_apple_2 - weight: 20 - - key: golden_apple_3 - weight: 5 - - key: jump_potion - weight: 15 - - key: regen_potion - weight: 1 -health: 80 \ No newline at end of file diff --git a/src/main/resources/speed-builders.yml b/src/main/resources/speed-builders.yml deleted file mode 100644 index e2bb29b..0000000 --- a/src/main/resources/speed-builders.yml +++ /dev/null @@ -1,41 +0,0 @@ -structures: - # Easy difficulty - portal: - difficulty: EASY - bed: - difficulty: EASY - farm: - difficulty: EASY - japanese_idk: - difficulty: EASY - mushroom: - difficulty: EASY - well: - difficulty: EASY - # Medium difficulty - bookshelves: - difficulty: MEDIUM - dragon: - difficulty: MEDIUM - enchanting: - difficulty: MEDIUM - bigportal: - difficulty: MEDIUM - warrior: - difficulty: MEDIUM - # Hard difficulty - tree: - difficulty: HARD - arcade: - difficulty: HARD - boat: - difficulty: HARD - car: - difficulty: HARD - graveyard: - difficulty: HARD - # Insane difficulty - outpost: - difficulty: INSANE - end_city: - difficulty: INSANE diff --git a/src/main/resources/speedbuilders/car.nbt b/src/main/resources/speedbuilders/car.nbt deleted file mode 100644 index 49970d9..0000000 Binary files a/src/main/resources/speedbuilders/car.nbt and /dev/null differ diff --git a/src/main/resources/speedbuilders/dragon.nbt b/src/main/resources/speedbuilders/dragon.nbt deleted file mode 100644 index b61aa3d..0000000 Binary files a/src/main/resources/speedbuilders/dragon.nbt and /dev/null differ diff --git a/src/main/resources/speedbuilders/enchanting.nbt b/src/main/resources/speedbuilders/enchanting.nbt deleted file mode 100644 index a2670ba..0000000 Binary files a/src/main/resources/speedbuilders/enchanting.nbt and /dev/null differ diff --git a/src/main/resources/speedbuilders/speedbuilders_template.nbt b/src/main/resources/speedbuilders/speedbuilders_template.nbt deleted file mode 100644 index 7d06b87..0000000 Binary files a/src/main/resources/speedbuilders/speedbuilders_template.nbt and /dev/null differ diff --git a/src/main/resources/speedbuilders/structure_template.nbt b/src/main/resources/speedbuilders/structure_template.nbt deleted file mode 100644 index 50d241f..0000000 Binary files a/src/main/resources/speedbuilders/structure_template.nbt and /dev/null differ diff --git a/src/main/resources/speedbuilders/warrior.nbt b/src/main/resources/speedbuilders/warrior.nbt deleted file mode 100644 index 1a9a7d8..0000000 Binary files a/src/main/resources/speedbuilders/warrior.nbt and /dev/null differ diff --git a/src/main/resources/speedbuilders/well.nbt b/src/main/resources/speedbuilders/well.nbt deleted file mode 100644 index 5d3e6aa..0000000 Binary files a/src/main/resources/speedbuilders/well.nbt and /dev/null differ diff --git a/src/speed-builders-schema.json b/src/speed-builders-schema.json deleted file mode 100644 index 91c22d6..0000000 --- a/src/speed-builders-schema.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "structures": { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9_]+$": { - "type": "object", - "properties": { - "difficulty": { - "type": "string", - "description": "Difficulty of the structure.", - "enum": [ - "EASY", - "MEDIUM", - "HARD", - "INSANE" - ] - } - }, - "required": [ - "difficulty" - ], - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - "required": [ - "structures" - ], - "additionalProperties": false -} \ No newline at end of file diff --git a/test-minigame/build.gradle.kts b/test-minigame/build.gradle.kts new file mode 100644 index 0000000..fc7b5f8 --- /dev/null +++ b/test-minigame/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + id("com.github.johnrengelman.shadow") version "8.1.1" + id("io.papermc.paperweight.userdev") + java +} + +group = "info.mester.network.testminigame" +version = "1.0" + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://oss.sonatype.org/content/groups/public/") +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(kotlin("reflect")) + compileOnly(project(":pgame-api")) + + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") +} +val targetJavaVersion = 21 +kotlin { + jvmToolchain(targetJavaVersion) +} + +tasks { + processResources { + val props = mapOf("version" to version) + inputs.properties(props) + filteringCharset = "UTF-8" + filesMatching("paper-plugin.yml") { + expand(props) + } + } + + build { + dependsOn("shadowJar") + } + + test { + useJUnitPlatform() + } +} + +sourceSets { + main { + java { + srcDir("src/main/kotlin") + } + } + test { + java { + srcDir("src/test/kotlin") + } + } +} diff --git a/test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java b/test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java new file mode 100644 index 0000000..3058b0c --- /dev/null +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java @@ -0,0 +1,76 @@ +package info.mester.network.testminigame; + +import info.mester.network.partygames.api.Game; +import info.mester.network.partygames.api.Minigame; +import io.papermc.paper.event.player.PrePlayerAttackEntityEvent; +import net.kyori.adventure.text.Component; +import org.bukkit.GameMode; +import org.bukkit.Material; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class JavaMinigame extends Minigame { + public JavaMinigame(@NotNull Game game) { + super(game, "java"); + } + + // to enable players damaging entities, you need to uncancel prePlayerAttackEntity + @Override + public void handlePrePlayerAttack(@NotNull PrePlayerAttackEntityEvent event) { + event.setCancelled(false); + } + + @Override + public void handlePlayerDeath(@NotNull PlayerDeathEvent event) { + event.setCancelled(true); + + Player player = event.getEntity(); + player.setGameMode(GameMode.SPECTATOR); + @SuppressWarnings("UnstableApiUsage") + Entity killer = event.getDamageSource().getCausingEntity(); + if (killer instanceof Player killerPlayer) { + // only award points to players + killerPlayer.sendMessage(Component.text("You killed " + player.getName() + "!")); + getGame().addScore(killerPlayer, 10, "Killed " + player.getName()); + } + + // get every online player in the game in survival mode to look for a last player standing + var remainingPlayer = getOnlinePlayers().stream().filter(Objects::nonNull).filter(p -> p.getGameMode() == GameMode.SURVIVAL).count(); + if (remainingPlayer <= 1) { + end(); + } + } + + + @Override + public void start() { + super.start(); + + // everything works as expected + getAudience().sendMessage(Component.text("Hello from Java!")); + + for (var player : getOnlinePlayers()) { + assert player != null; // this is very silly because getOnlinePlayers() never returns null elements, but Java is gonna do Java stuff + player.getInventory().addItem(ItemStack.of(Material.DIAMOND_SWORD)); + player.sendMessage(Component.text("Hello from Java!")); + } + + // create a countdown without the bar on top of the screen + startCountdown(20 * 20, false, this::end); + } + + @Override + public @NotNull Component getName() { + return Component.text("Java Minigame"); + } + + @Override + public @NotNull Component getDescription() { + return Component.text("This is a minigame written in Java to show interoperability with the API written in Kotlin!"); + } +} diff --git a/test-minigame/src/main/kotlin/info/mester/network/testminigame/MinigamePlugin.kt b/test-minigame/src/main/kotlin/info/mester/network/testminigame/MinigamePlugin.kt new file mode 100644 index 0000000..084ccf4 --- /dev/null +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/MinigamePlugin.kt @@ -0,0 +1,39 @@ +package info.mester.network.testminigame + +import info.mester.network.partygames.api.MinigameWorld +import info.mester.network.partygames.api.PartyGamesCore +import org.bukkit.plugin.java.JavaPlugin +import org.bukkit.util.Vector + +class MinigamePlugin : JavaPlugin() { + override fun onEnable() { + val core = PartyGamesCore.getInstance() + core.gameRegistry.registerMinigame( + this, + PlaceBlockMinigame::class.qualifiedName!!, + "place_block", + listOf( + MinigameWorld("placeblock", Vector(0.5, 60.0, 0.5)), + ), + "Place Block", + ) + core.gameRegistry.registerMinigame( + this, + SimpleMinigame::class.qualifiedName!!, + "simple", + listOf( + MinigameWorld("simple", Vector(0.5, 60.0, 0.5)), + ), + "Simple Minigame", + ) + core.gameRegistry.registerMinigame( + this, + JavaMinigame::class.qualifiedName!!, + "java", + listOf( + MinigameWorld("java", Vector(0.5, 60.0, 0.5)), + ), + "Java Minigame", + ) + } +} diff --git a/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt b/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt new file mode 100644 index 0000000..bd6036a --- /dev/null +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt @@ -0,0 +1,43 @@ +package info.mester.network.testminigame + +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.Minigame +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Material +import org.bukkit.event.block.BlockPlaceEvent +import org.bukkit.inventory.ItemStack + +class PlaceBlockMinigame( + game: Game, +) : Minigame(game, "place_block") { + override fun onLoad() { + // access the world using game.world + val world = game.world + world.worldBorder.size = 30.0 + world.worldBorder.center = startPos + super.onLoad() + } + + override fun start() { + super.start() + + audience.sendMessage(Component.text("Place blocks to win!", NamedTextColor.AQUA)) + // get every player in the game with onlinePlayers + for (player in onlinePlayers) { + player.inventory.addItem(ItemStack.of(Material.OBSIDIAN, 64)) + } + + startCountdown(20 * 20, false) { + end() + } + } + + // override functions to add custom functionality to the minigame + override fun handleBlockPlace(event: BlockPlaceEvent) { + game.addScore(event.player, 1, "Placed a block") + } + + override val name = Component.text("Place Block", NamedTextColor.AQUA) + override val description = Component.text("Place blocks to win!", NamedTextColor.AQUA) +} diff --git a/test-minigame/src/main/kotlin/info/mester/network/testminigame/SimpleMinigame.kt b/test-minigame/src/main/kotlin/info/mester/network/testminigame/SimpleMinigame.kt new file mode 100644 index 0000000..98ddd22 --- /dev/null +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/SimpleMinigame.kt @@ -0,0 +1,26 @@ +package info.mester.network.testminigame + +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.Minigame +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor + +class SimpleMinigame( + game: Game, +) : Minigame(game, "simple") { + override fun start() { + super.start() + // use audience to send messages to all players in the game + audience.sendMessage(Component.text("Welcome to Simple Minigame!", NamedTextColor.YELLOW)) + // use startCountdown to create a countdown timer with a bar on top of the screen + // it takes in a tick duration, so 20 seconds is 20 * 20 = 400 ticks + startCountdown(20 * 20) { + end() + } + } + + // override name and description to change the display name and description of the minigame + override val name = Component.text("Simple Minigame", NamedTextColor.AQUA) + override val description = + Component.text("Simple Minigame, super boring but generic, which is somehow good?", NamedTextColor.AQUA) +} diff --git a/test-minigame/src/main/resources/paper-plugin.yml b/test-minigame/src/main/resources/paper-plugin.yml new file mode 100644 index 0000000..439f1fe --- /dev/null +++ b/test-minigame/src/main/resources/paper-plugin.yml @@ -0,0 +1,13 @@ +name: TestMinigame +version: "1.0" +main: info.mester.network.testminigame.MinigamePlugin +api-version: "1.21" +load: POSTWORLD +dependencies: + server: + PartyGamesCore: + load: BEFORE + required: true + join-classpath: true +authors: + - Mester \ No newline at end of file