From 411c89fb01ba56a418830375cd8ff416180fdfee Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Wed, 15 Jan 2025 21:06:05 +0100 Subject: [PATCH 01/18] Begin separation of api --- .github/workflows/build.yml | 2 +- .gitignore | 4 +- build.gradle.kts | 104 +--- pgame-api/build.gradle.kts | 62 +++ pgame-plugin/build.gradle.kts | 98 ++++ .../src}/SpeedBuildersWeightGraph.ggb | Bin {src => pgame-plugin/src}/config-schema.json | 0 .../src}/health-shop-schema.json | 0 .../mester/network/partygames/Bootstrapper.kt | 444 +++++++++--------- .../network/partygames/DatabaseManager.kt | 0 .../info/mester/network/partygames/Loader.kt | 0 .../mester/network/partygames/PartyGames.kt | 0 .../network/partygames/PartyListener.kt | 0 .../network/partygames/PlayingPlaceholder.kt | 0 .../mester/network/partygames/UUIDDataType.kt | 0 .../network/partygames/admin/AdminUtils.kt | 0 .../network/partygames/admin/GamesUI.kt | 0 .../network/partygames/admin/InvseeUI.kt | 0 .../network/partygames/admin/PlayerAdminUI.kt | 0 .../network/partygames/game/DamageDealer.kt | 0 .../mester/network/partygames/game/Game.kt | 0 .../network/partygames/game/GameManager.kt | 0 .../partygames/game/GardeningMinigame.kt | 0 .../partygames/game/HealthShopMinigame.kt | 0 .../partygames/game/IntroductionTimer.kt | 0 .../network/partygames/game/Minigame.kt | 0 .../mester/network/partygames/game/Queue.kt | 0 .../partygames/game/RunawayMinigame.kt | 0 .../partygames/game/SnifferHuntMinigame.kt | 0 .../partygames/game/SpeedBuildersMinigame.kt | 0 .../partygames/game/gardening/Cactus.kt | 0 .../partygames/game/gardening/GardenTap.kt | 0 .../partygames/game/gardening/Lilac.kt | 0 .../partygames/game/gardening/OakTree.kt | 0 .../partygames/game/gardening/Peony.kt | 0 .../partygames/game/gardening/Plant.kt | 0 .../game/gardening/RainbowFlower.kt | 0 .../network/partygames/game/gardening/Rose.kt | 0 .../partygames/game/gardening/Sunflower.kt | 0 .../network/partygames/game/gardening/Weed.kt | 0 .../partygames/game/gardening/ZombieWeed.kt | 0 .../partygames/game/healthshop/ArmorType.kt | 0 .../game/healthshop/HealthShopItem.kt | 0 .../game/healthshop/HealthShopUI.kt | 0 .../game/healthshop/SupplyChestTimer.kt | 0 .../game/snifferhunt/RideableSniffer.kt | 0 .../game/snifferhunt/SnifferHuntConfig.kt | 0 .../game/snifferhunt/TreasureMap.kt | 0 .../network/partygames/level/LevelData.kt | 0 .../network/partygames/level/LevelManager.kt | 0 .../partygames/level/LevelPlaceholder.kt | 0 .../sidebar/GameSidebarComponent.kt | 0 .../sidebar/LobbySidebarComponent.kt | 0 .../sidebar/QueueSidebarComponent.kt | 0 .../partygames/sidebar/SidebarManager.kt | 0 .../network/partygames/util/InventoryUtil.kt | 0 .../network/partygames/util/LocationUtils.kt | 0 .../network/partygames/util/WeightedItem.kt | 0 .../src}/main/resources/config.yml | 0 .../src}/main/resources/health-shop.yml | 0 .../src}/main/resources/paper-plugin.yml | 0 .../src}/main/resources/sniffer-hunt.yml | 0 .../src}/main/resources/speed-builders.yml | 0 .../main/resources/speedbuilders/arcade.nbt | Bin .../src}/main/resources/speedbuilders/bed.nbt | Bin .../resources/speedbuilders/bigportal.nbt | Bin .../main/resources/speedbuilders/boat.nbt | Bin .../resources/speedbuilders/bookshelves.nbt | Bin .../src}/main/resources/speedbuilders/car.nbt | Bin .../main/resources/speedbuilders/dragon.nbt | Bin .../resources/speedbuilders/enchanting.nbt | Bin .../main/resources/speedbuilders/end_city.nbt | Bin .../main/resources/speedbuilders/farm.nbt | Bin .../resources/speedbuilders/graveyard.nbt | Bin .../resources/speedbuilders/japanese_idk.nbt | Bin .../main/resources/speedbuilders/mushroom.nbt | Bin .../main/resources/speedbuilders/outpost.nbt | Bin .../main/resources/speedbuilders/portal.nbt | Bin .../speedbuilders/speedbuilders_template.nbt | Bin .../speedbuilders/structure_template.nbt | Bin .../main/resources/speedbuilders/tree.nbt | Bin .../main/resources/speedbuilders/warrior.nbt | Bin .../main/resources/speedbuilders/well.nbt | Bin .../src}/sniffer-hunt-schema.json | 0 .../src}/speed-builders-schema.json | 0 .../network/partygames/TestPartyGames.kt | 0 settings.gradle.kts | 5 +- 87 files changed, 398 insertions(+), 321 deletions(-) create mode 100644 pgame-api/build.gradle.kts create mode 100644 pgame-plugin/build.gradle.kts rename {src => pgame-plugin/src}/SpeedBuildersWeightGraph.ggb (100%) rename {src => pgame-plugin/src}/config-schema.json (100%) rename {src => pgame-plugin/src}/health-shop-schema.json (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/Bootstrapper.kt (98%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/DatabaseManager.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/Loader.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/PartyGames.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/PartyListener.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/UUIDDataType.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/admin/GamesUI.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/admin/InvseeUI.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/Game.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/GameManager.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/Minigame.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/Queue.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/Cactus.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/GardenTap.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/Lilac.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/OakTree.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/Peony.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/Plant.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/RainbowFlower.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/Rose.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/Sunflower.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/Weed.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/gardening/ZombieWeed.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/healthshop/ArmorType.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopItem.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopUI.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/healthshop/SupplyChestTimer.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/snifferhunt/RideableSniffer.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/snifferhunt/SnifferHuntConfig.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/game/snifferhunt/TreasureMap.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/level/LevelData.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/level/LevelManager.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/level/LevelPlaceholder.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/sidebar/LobbySidebarComponent.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/sidebar/QueueSidebarComponent.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/util/LocationUtils.kt (100%) rename {src => pgame-plugin/src}/main/kotlin/info/mester/network/partygames/util/WeightedItem.kt (100%) rename {src => pgame-plugin/src}/main/resources/config.yml (100%) rename {src => pgame-plugin/src}/main/resources/health-shop.yml (100%) rename {src => pgame-plugin/src}/main/resources/paper-plugin.yml (100%) rename {src => pgame-plugin/src}/main/resources/sniffer-hunt.yml (100%) rename {src => pgame-plugin/src}/main/resources/speed-builders.yml (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/arcade.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/bed.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/bigportal.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/boat.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/bookshelves.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/car.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/dragon.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/enchanting.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/end_city.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/farm.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/graveyard.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/japanese_idk.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/mushroom.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/outpost.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/portal.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/speedbuilders_template.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/structure_template.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/tree.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/warrior.nbt (100%) rename {src => pgame-plugin/src}/main/resources/speedbuilders/well.nbt (100%) rename {src => pgame-plugin/src}/sniffer-hunt-schema.json (100%) rename {src => pgame-plugin/src}/speed-builders-schema.json (100%) rename {src => pgame-plugin/src}/test/kotlin/info/mester/network/partygames/TestPartyGames.kt (100%) 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..d802ef9 100644 --- a/.gitignore +++ b/.gitignore @@ -112,13 +112,13 @@ gradle-app.setting **/build/ # Common working directory -run/ +pgame-plugin/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/build.gradle.kts b/build.gradle.kts index 946cd13..779697f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,92 +1,19 @@ 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("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")) +val targetJavaVersion = 21 +subprojects { + apply(plugin = "org.jetbrains.kotlin.jvm") } sonar { @@ -95,16 +22,3 @@ sonar { property("sonar.projectName", "Bedless Tournament") } } - -sourceSets { - main { - java { - srcDir("src/main/kotlin") - } - } - test { - java { - srcDir("src/test/kotlin") - } - } -} diff --git a/pgame-api/build.gradle.kts b/pgame-api/build.gradle.kts new file mode 100644 index 0000000..f3eb0e7 --- /dev/null +++ b/pgame-api/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + id("com.github.johnrengelman.shadow") version "8.1.1" + id("io.papermc.paperweight.userdev") version "2.0.0-beta.13" + java +} + +group = "info.mester.network.partygames" +version = "a1.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.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")) + + 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/pgame-plugin/build.gradle.kts b/pgame-plugin/build.gradle.kts new file mode 100644 index 0000000..46db53e --- /dev/null +++ b/pgame-plugin/build.gradle.kts @@ -0,0 +1,98 @@ +plugins { + id("com.github.johnrengelman.shadow") version "8.1.1" + id("io.papermc.paperweight.userdev") version "2.0.0-beta.13" + java +} + +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.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")) + implementation(project(":pgame-api")) + + 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() + } +} + +tasks.register("copyPluginToRun") { + dependsOn("build") + from(buildDir.resolve("libs").resolve("partygames-${project.version}-all.jar")) + into(rootDir.resolve("run").resolve("plugins")) +} + +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/src/config-schema.json b/pgame-plugin/src/config-schema.json similarity index 100% rename from src/config-schema.json rename to pgame-plugin/src/config-schema.json diff --git a/src/health-shop-schema.json b/pgame-plugin/src/health-shop-schema.json similarity index 100% rename from src/health-shop-schema.json rename to pgame-plugin/src/health-shop-schema.json diff --git a/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt similarity index 98% rename from src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt index 9dcb495..ff3c084 100644 --- a/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt @@ -1,222 +1,222 @@ -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 + +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 +} diff --git a/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt diff --git a/src/main/kotlin/info/mester/network/partygames/Loader.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Loader.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/Loader.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/Loader.kt 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 100% rename from src/main/kotlin/info/mester/network/partygames/PartyGames.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt diff --git a/src/main/kotlin/info/mester/network/partygames/PartyListener.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/PartyListener.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt diff --git a/src/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt 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/admin/AdminUtils.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt diff --git a/src/main/kotlin/info/mester/network/partygames/admin/GamesUI.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/GamesUI.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/admin/GamesUI.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/GamesUI.kt diff --git a/src/main/kotlin/info/mester/network/partygames/admin/InvseeUI.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/InvseeUI.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/admin/InvseeUI.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/InvseeUI.kt diff --git a/src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.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/DamageDealer.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt diff --git a/src/main/kotlin/info/mester/network/partygames/game/Game.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Game.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/Game.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Game.kt diff --git a/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/GameManager.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt 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 100% 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 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 100% 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 diff --git a/src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt diff --git a/src/main/kotlin/info/mester/network/partygames/game/Minigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Minigame.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/Minigame.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Minigame.kt 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 100% 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 diff --git a/src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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/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 similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopItem.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopItem.kt diff --git a/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 similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopUI.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopUI.kt 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 100% 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 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 100% 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 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/level/LevelPlaceholder.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/level/LevelPlaceholder.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/level/LevelPlaceholder.kt 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 100% 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 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 100% 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 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 100% 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 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 100% 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 diff --git a/src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt similarity index 100% rename from src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt 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 100% 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 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 100% 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 diff --git a/src/main/resources/config.yml b/pgame-plugin/src/main/resources/config.yml similarity index 100% rename from src/main/resources/config.yml rename to pgame-plugin/src/main/resources/config.yml diff --git a/src/main/resources/health-shop.yml b/pgame-plugin/src/main/resources/health-shop.yml similarity index 100% rename from src/main/resources/health-shop.yml rename to pgame-plugin/src/main/resources/health-shop.yml diff --git a/src/main/resources/paper-plugin.yml b/pgame-plugin/src/main/resources/paper-plugin.yml similarity index 100% rename from src/main/resources/paper-plugin.yml rename to pgame-plugin/src/main/resources/paper-plugin.yml 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/src/main/resources/speed-builders.yml b/pgame-plugin/src/main/resources/speed-builders.yml similarity index 100% rename from src/main/resources/speed-builders.yml rename to pgame-plugin/src/main/resources/speed-builders.yml 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/src/main/resources/speedbuilders/car.nbt b/pgame-plugin/src/main/resources/speedbuilders/car.nbt similarity index 100% rename from src/main/resources/speedbuilders/car.nbt rename to pgame-plugin/src/main/resources/speedbuilders/car.nbt diff --git a/src/main/resources/speedbuilders/dragon.nbt b/pgame-plugin/src/main/resources/speedbuilders/dragon.nbt similarity index 100% rename from src/main/resources/speedbuilders/dragon.nbt rename to pgame-plugin/src/main/resources/speedbuilders/dragon.nbt diff --git a/src/main/resources/speedbuilders/enchanting.nbt b/pgame-plugin/src/main/resources/speedbuilders/enchanting.nbt similarity index 100% rename from src/main/resources/speedbuilders/enchanting.nbt rename to pgame-plugin/src/main/resources/speedbuilders/enchanting.nbt 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/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/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/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/src/main/resources/speedbuilders/speedbuilders_template.nbt b/pgame-plugin/src/main/resources/speedbuilders/speedbuilders_template.nbt similarity index 100% rename from src/main/resources/speedbuilders/speedbuilders_template.nbt rename to pgame-plugin/src/main/resources/speedbuilders/speedbuilders_template.nbt diff --git a/src/main/resources/speedbuilders/structure_template.nbt b/pgame-plugin/src/main/resources/speedbuilders/structure_template.nbt similarity index 100% rename from src/main/resources/speedbuilders/structure_template.nbt rename to pgame-plugin/src/main/resources/speedbuilders/structure_template.nbt 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/src/main/resources/speedbuilders/warrior.nbt b/pgame-plugin/src/main/resources/speedbuilders/warrior.nbt similarity index 100% rename from src/main/resources/speedbuilders/warrior.nbt rename to pgame-plugin/src/main/resources/speedbuilders/warrior.nbt diff --git a/src/main/resources/speedbuilders/well.nbt b/pgame-plugin/src/main/resources/speedbuilders/well.nbt similarity index 100% rename from src/main/resources/speedbuilders/well.nbt rename to pgame-plugin/src/main/resources/speedbuilders/well.nbt 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/src/speed-builders-schema.json b/pgame-plugin/src/speed-builders-schema.json similarity index 100% rename from src/speed-builders-schema.json rename to pgame-plugin/src/speed-builders-schema.json 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..539dc4c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,4 +5,7 @@ pluginManagement { gradlePluginPortal() maven("https://repo.papermc.io/repository/maven-public/") } -} \ No newline at end of file +} + +include("pgame-api") +include("pgame-plugin") From 16ad7436571435125dd2b0b47934ef57a05c9a18 Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Wed, 15 Jan 2025 21:09:18 +0100 Subject: [PATCH 02/18] Update README.md with project structure --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e6d57e..87de208 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,20 @@ - 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 + +## 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. + +### 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. \ No newline at end of file From 9b36cb5864f1741f3403bc5bcaa1abba62b66431 Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Thu, 16 Jan 2025 18:45:26 +0100 Subject: [PATCH 03/18] Move some relevant code into the API (currently broken, don't use) --- .gitignore | 2 +- pgame-api/build.gradle.kts | 20 +- .../mester/network/partygames/api}/Game.kt | 49 ++- .../network/partygames/api/GameRegistry.kt | 72 ++++ .../partygames/api/IntroductionTimer.kt | 93 +++++ .../network/partygames/api}/Minigame.kt | 5 +- .../network/partygames/api/PartyGamesCore.kt | 91 ++++ .../partygames/api/PartyGamesListener.kt | 390 ++++++++++++++++++ .../network/partygames/api}/admin/GamesUI.kt | 2 +- .../network/partygames/api}/admin/InvseeUI.kt | 2 +- .../partygames/api}/admin/PlayerAdminUI.kt | 2 +- .../partygames/api/events/GameStartedEvent.kt | 17 + pgame-api/src/main/resources/paper-plugin.yml | 13 + pgame-plugin/build.gradle.kts | 23 +- .../mester/network/partygames/Bootstrapper.kt | 4 +- .../mester/network/partygames/PartyGames.kt | 58 +-- .../network/partygames/PartyListener.kt | 4 +- .../network/partygames/admin/AdminUtils.kt | 105 ----- .../network/partygames/game/DamageDealer.kt | 2 + .../network/partygames/game/GameManager.kt | 24 -- .../partygames/game/IntroductionTimer.kt | 90 ---- .../src/main/resources/paper-plugin.yml | 2 +- 22 files changed, 751 insertions(+), 319 deletions(-) rename {pgame-plugin/src/main/kotlin/info/mester/network/partygames/game => pgame-api/src/main/kotlin/info/mester/network/partygames/api}/Game.kt (92%) create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/GameRegistry.kt create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt rename {pgame-plugin/src/main/kotlin/info/mester/network/partygames/game => pgame-api/src/main/kotlin/info/mester/network/partygames/api}/Minigame.kt (98%) create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesCore.kt create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt rename {pgame-plugin/src/main/kotlin/info/mester/network/partygames => pgame-api/src/main/kotlin/info/mester/network/partygames/api}/admin/GamesUI.kt (97%) rename {pgame-plugin/src/main/kotlin/info/mester/network/partygames => pgame-api/src/main/kotlin/info/mester/network/partygames/api}/admin/InvseeUI.kt (97%) rename {pgame-plugin/src/main/kotlin/info/mester/network/partygames => pgame-api/src/main/kotlin/info/mester/network/partygames/api}/admin/PlayerAdminUI.kt (98%) create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameStartedEvent.kt create mode 100644 pgame-api/src/main/resources/paper-plugin.yml delete mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt diff --git a/.gitignore b/.gitignore index d802ef9..c88ecc4 100644 --- a/.gitignore +++ b/.gitignore @@ -112,7 +112,7 @@ gradle-app.setting **/build/ # Common working directory -pgame-plugin/run/ +run # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar diff --git a/pgame-api/build.gradle.kts b/pgame-api/build.gradle.kts index f3eb0e7..af91cee 100644 --- a/pgame-api/build.gradle.kts +++ b/pgame-api/build.gradle.kts @@ -13,9 +13,6 @@ repositories { 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 { @@ -23,6 +20,7 @@ dependencies { implementation(kotlin("reflect")) paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") + compileOnly("com.infernalsuite.aswm:api:3.0.0-SNAPSHOT") } val targetJavaVersion = 21 kotlin { @@ -48,6 +46,22 @@ tasks { } } +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 { diff --git a/pgame-plugin/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 92% rename from pgame-plugin/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..8da03a8 100644 --- a/pgame-plugin/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,8 @@ -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.GameStartedEvent import net.kyori.adventure.audience.Audience import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.key.Key @@ -20,6 +18,10 @@ import java.util.UUID import java.util.logging.Level import kotlin.math.floor +private fun UUID.shorten() = this.toString().replace("-", "") + +private val mm = MiniMessage.miniMessage() + data class PlayerData( var score: Int, ) @@ -52,8 +54,8 @@ enum class GameState { } class Game( - private val plugin: PartyGames, - val type: GameType, + private val core: PartyGamesCore, + val bundle: MinigameBundle, players: List, ) { companion object { @@ -190,7 +192,7 @@ class Game( if (player.isOnline) { resetPlayer(player) sidebarManager.openLobbySidebar(player) - player.teleport(plugin.spawnLocation) + player.teleport(plugin.spawnLocation) // TODO: replace with PlayerRemovedFromGameEvent } if (playerDatas.isEmpty()) { end() @@ -210,17 +212,24 @@ class Game( // 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() + .map { + core.gameRegistry + .getMinigame(it)!! + .minigame.constructors + .first() + .call(this) + }.toTypedArray() // update the playing placeholder - plugin.playingPlaceholder.addPlaying(type.name, players.size) + plugin.playingPlaceholder.addPlaying(type.name, players.size) // TODO: use the event try { val success = nextMinigame() if (!success) { throw IllegalStateException("Couldn't load the first minigame!") } + val event = GameStartedEvent(this) + event.callEvent() // wait a tick and set up the sidebar Bukkit.getScheduler().runTaskLater( plugin, @@ -233,7 +242,7 @@ class Game( ) } 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() } @@ -257,13 +266,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 gameWorld = minigameWorld.clone(worldName) // now switch to sync mode Bukkit.getScheduler().runTask( - plugin, + core, Runnable { startIntroduction(gameWorld) }, @@ -291,14 +300,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 @@ -329,7 +338,7 @@ class Game( } // wait for 5 seconds and load the new minigame Bukkit.getScheduler().runTaskLater( - plugin, + core, Runnable { val success = nextMinigame() if (!success) { @@ -366,7 +375,7 @@ class Game( */ fun terminate() { _state = GameState.STOPPED - plugin.playingPlaceholder.removePlaying(type.name, playerDatas.size) + plugin.playingPlaceholder.removePlaying(type.name, playerDatas.size) // TODO: replace with GameTerminatedEvent // this could be the case if we forcefully end the tournament with the command runningMinigame?.terminate() _runningMinigame = null @@ -421,7 +430,7 @@ class Game( 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) + val oldLevel = plugin.levelManager.levelDataOf(player.uniqueId) // TODO: replace with GameEndedEvent plugin.levelManager.addXp(player.uniqueId, data.score.coerceAtLeast(0)) val newLevel = plugin.levelManager.levelDataOf(player.uniqueId) val levelUpMessage = @@ -469,7 +478,7 @@ class Game( fun handleRejoin(player: Player) { resetPlayer(player) - plugin.sidebarManager.openGameSidebar(player) + plugin.sidebarManager.openGameSidebar(player) // TODO: replace with PlayerRejoinedEvent 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..8e74541 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/GameRegistry.kt @@ -0,0 +1,72 @@ +package info.mester.network.partygames.api + +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 RegisteredMinigame( + val plugin: JavaPlugin, + val minigame: KClass, + val name: String, + val showInJoin: Boolean, + val startPos: Vector, +) + +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, + minigame: KClass, + name: String, + startPos: Vector, + showInJoin: Boolean, + register: String?, + ) { + val registeredMinigame = RegisteredMinigame(plugin, minigame, name, showInJoin, startPos) + minigames.add(registeredMinigame) + if (register != null) { + bundles.add(MinigameBundle(plugin, listOf(name), name, register)) + } + } + + fun registerBundle( + plugin: JavaPlugin, + minigames: List, + name: String, + displayName: String, + ) { + bundles.add(MinigameBundle(plugin, minigames, name, displayName)) + } + + fun getStartPos(name: String): Vector? { + val minigame = minigames.firstOrNull { it.name == name } + return minigame?.startPos + } + + fun getMinigame(name: String): RegisteredMinigame? = minigames.firstOrNull { it.name == name } + + fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name } + + 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 + } +} diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt new file mode 100644 index 0000000..1cdf272 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt @@ -0,0 +1,93 @@ +package info.mester.network.partygames.api + +import net.kyori.adventure.audience.Audience +import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.scheduler.BukkitTask +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 + +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 { + val percentage = (1 - (remainingTime.toDouble() / (INTRODUCTION_TIME * 20))) * 100 + val filledSquares = ((percentage + 5) / 10).toInt().coerceIn(0, 10) + + return buildString { + if (filledSquares > 0) { + append("") + append("■".repeat(filledSquares)) + append("") + } + if (filledSquares < 10) { + append("") + append("■".repeat(10 - filledSquares)) + append("") + } + } + } + + override fun accept(t: BukkitTask) { + val minigame = game.runningMinigame + if (game.state != GameState.PRE_GAME || minigame == null) { + t.cancel() + return + } + 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 + if (rotation > 360.0) { + rotation = 0.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 + // basically, offset the degree by the player's index in the list + val offset = players.indexOf(player) * 360.0 / players.size + val playerRotation = (rotation + offset) % 360.0 + // shoot a line from startpos with the angle and store the hit position + val radians = Math.toRadians(playerRotation) + val hitX = 15.0 * cos(radians) + val hitZ = 15.0 * sin(radians) + // 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 + } + + player.teleportAsync(finalPos) + } + } +} diff --git a/pgame-plugin/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 98% rename from pgame-plugin/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..ad1ee9c 100644 --- a/pgame-plugin/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,5 @@ -package info.mester.network.partygames.game +package info.mester.network.partygames.api -import info.mester.network.partygames.PartyGames import io.papermc.paper.event.entity.EntityMoveEvent import io.papermc.paper.event.player.AsyncChatEvent import io.papermc.paper.event.player.PrePlayerAttackEntityEvent @@ -36,7 +35,7 @@ abstract class Minigame( ) { 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 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..f03f2ae --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesCore.kt @@ -0,0 +1,91 @@ +package info.mester.network.partygames.api + +import org.bukkit.Bukkit +import org.bukkit.entity.Entity +import org.bukkit.entity.Player +import org.bukkit.plugin.java.JavaPlugin +import java.util.UUID + +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() + } +} diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt new file mode 100644 index 0000000..4b1440d --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt @@ -0,0 +1,390 @@ +package info.mester.network.partygames.api + +import com.destroystokyo.paper.event.block.AnvilDamagedEvent +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 +import org.bukkit.entity.AbstractArrow +import org.bukkit.entity.Arrow +import org.bukkit.entity.EntityType +import org.bukkit.entity.Player +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.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.FoodLevelChangeEvent +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.inventory.InventoryType +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.PlayerJoinEvent +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 + +class PartyGamesListener( + private val core: PartyGamesCore, +) : Listener { + private val gameRegistry = core.gameRegistry + + private fun getMinigameFromWorld(world: World) = gameRegistry.getGameByWorld(world)?.runningMinigame + + @EventHandler + fun onPlayerInteractAtEntity(event: PlayerInteractAtEntityEvent) { + if (event.hand == EquipmentSlot.OFF_HAND) { + return + } + // check if the player is an admin and if they right-clicked a player while in a game + val game = gameRegistry.getGameByWorld(event.player.world) + if (core.isAdmin(event.player) && + event.rightClicked is Player && + game != null + ) { + event.isCancelled = true + // setup admin ui + val playerAdminUI = PlayerAdminUI(game, event.rightClicked as Player) + event.player.openInventory(playerAdminUI.inventory) + return + } + // execute the event on the minigame + val minigame = getMinigameFromWorld(event.player.world) + minigame?.handlePlayerInteractAtEntity(event) + } + + @EventHandler + fun onInventoryClick(event: InventoryClickEvent) { + val clickedInventory = event.clickedInventory ?: return + val holder = clickedInventory.getHolder(false) + 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 + return + } + } + + if (holder is PlayerAdminUI) { + event.isCancelled = true + 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 + if (event.entity.type == EntityType.PLAYER && event.cause == EntityDamageEvent.DamageCause.FALL) { + event.isCancelled = true + return + } + } + + @EventHandler + fun onFoodLevelChange(event: FoodLevelChangeEvent) { + if (event.entity.type == EntityType.PLAYER) { + event.isCancelled = true + val player = event.entity as Player + player.foodLevel = 20 + player.saturation = 0f + player.sendHealthUpdate() + } + } + + @EventHandler + fun onAsyncChat(event: AsyncChatEvent) { + if (PartyGames.plugin.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 = 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") + } + // 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) + } + + @EventHandler + fun onPrePlayerAttack(event: PrePlayerAttackEntityEvent) { + // by default disable the event for every non-admin player + if (plugin.isAdmin(event.player)) { + return + } + event.isCancelled = true + // however, the minigame may override the cancellation, allowing the event + val minigame = getMinigameFromWorld(event.player.world) ?: return + minigame.handlePrePlayerAttack(event) + } + + @EventHandler + fun onInventoryClose(event: InventoryCloseEvent) { + if (plugin.isAdmin(event.player)) { + return + } + val minigame = getMinigameFromWorld(event.player.world) + minigame?.handleInventoryClose(event) + } + + @EventHandler + fun onPlayerMove(event: PlayerMoveEvent) { + if (plugin.isAdmin(event.player)) { + return + } + val minigame = getMinigameFromWorld(event.player.world) + minigame?.handlePlayerMove(event) + } + + @EventHandler + fun onPlayerQuit(event: PlayerQuitEvent) { + plugin.setAdmin(event.player, false) + gameRegistry.getQueueOf(event.player)?.removePlayer(event.player) + gameRegistry.getGameOf(event.player)?.handleDisconnect(event.player, false) + plugin.sidebarManager.unregisterPlayer(event.player) + } + + @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) + } + // 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 + gameRegistry.getGameOf(event.player)?.handleRejoin(event.player) + } + + @EventHandler + fun onBlockBreak(event: BlockBreakEvent) { + if (plugin.isAdmin(event.player)) { + return + } + val minigame = getMinigameFromWorld(event.player.world) ?: return + event.isCancelled = true // only cancel the event if we're in a minigame + minigame.handleBlockBreak(event) + } + + @EventHandler + fun onEntityRegainHealth(event: EntityRegainHealthEvent) { + if (plugin.isAdmin(event.entity)) { + return + } + if (event.entityType != EntityType.PLAYER) { + return + } + val runningMinigame = getMinigameFromWorld(event.entity.world) + if (runningMinigame is HealthShopMinigame) { + runningMinigame.handleEntityRegainHealth(event) + } + } + + @EventHandler + fun onEntityDamageByEntity(event: EntityDamageByEntityEvent) { + val minigame = getMinigameFromWorld(event.entity.world) + minigame?.handleEntityDamageByEntity(event) + } + + @EventHandler + fun onPlayerDeath(event: PlayerDeathEvent) { + val minigame = getMinigameFromWorld(event.entity.world) + minigame?.handlePlayerDeath(event) + } + + @EventHandler + fun onPlayerItemConsume(event: PlayerItemConsumeEvent) { + val minigame = getMinigameFromWorld(event.player.world) + if (minigame is HealthShopMinigame) { + minigame.handlePlayerItemConsume(event) + } + } + + @EventHandler + fun onPlayerDropItem(event: PlayerDropItemEvent) { + if (plugin.isAdmin(event.player)) { + return + } + + event.isCancelled = true + val minigame = getMinigameFromWorld(event.player.world) + minigame?.handlePlayerDropItem(event) + } + + @EventHandler + fun onBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) { + if (plugin.isAdmin(event.entity)) { + return + } + val minigame = getMinigameFromWorld(event.entity.world) + if (minigame is SpeedBuildersMinigame) { + minigame.handleBlockBreakProgressUpdate(event) + } + } + + @EventHandler + fun onBlockPlace(event: BlockPlaceEvent) { + if (plugin.isAdmin(event.player)) { + return + } + val minigame = getMinigameFromWorld(event.player.world) + minigame?.handleBlockPlace(event) + } + + @EventHandler + fun onArrowBodyCountChange(event: ArrowBodyCountChangeEvent) { + // players should not have arrows stuck in their butts + event.newAmount = 0 + } + + @EventHandler + fun onEntityShootBow(event: EntityShootBowEvent) { + if (plugin.isAdmin(event.entity)) { + return + } + // don't allow arrows from being picked up + val projectile = event.projectile + if (projectile is Arrow) { + projectile.pickupStatus = AbstractArrow.PickupStatus.CREATIVE_ONLY + } + val minigame = getMinigameFromWorld(event.entity.world) + if (minigame is HealthShopMinigame) { + minigame.handleEntityShootBow(event) + } + } + + @EventHandler + fun onPlayerInteract(event: PlayerInteractEvent) { + if (PartyGames.plugin.isAdmin(event.player)) { + return + } + plugin.gameManager.getQueueOf(event.player)?.handlePlayerInteract(event) + getMinigameFromWorld(event.player.world)?.handlePlayerInteract(event) + } + + @EventHandler + fun onEntityMove(event: EntityMoveEvent) { + val minigame = getMinigameFromWorld(event.entity.world) + minigame?.handleEntityMove(event) + } + + @EventHandler + fun onBlockPhysics(event: BlockPhysicsEvent) { + val minigame = getMinigameFromWorld(event.block.world) + minigame?.handleBlockPhysics(event) + } + + @EventHandler + fun onEntityCombust(event: EntityCombustEvent) { + val minigame = getMinigameFromWorld(event.entity.world) + minigame?.handleEntityCombust(event) + } + + @EventHandler + fun onWorldLoad(event: WorldLoadEvent) { + val world = event.world + PartyGames.initWorld(world) + } + + @EventHandler + fun onPlayerSwapHandItems(event: PlayerSwapHandItemsEvent) { + event.isCancelled = true + } + + @EventHandler + fun onEntityChangeBlock(event: EntityChangeBlockEvent) { + val runningMinigame = getMinigameFromWorld(event.entity.world) + runningMinigame?.handleEntityChangeBlock(event) + } + + @EventHandler + fun onInventoryOpen(event: InventoryOpenEvent) { + val runningMinigame = getMinigameFromWorld(event.player.world) + runningMinigame?.handleInventoryOpen(event) + } + + @EventHandler + fun onPlayerToogleFlight(event: PlayerToggleFlightEvent) { + val runningMinigame = getMinigameFromWorld(event.player.world) + runningMinigame?.handlePlayerToggleFlight(event) + } + + @EventHandler + fun onEntityDismount(event: EntityDismountEvent) { + val runningMinigame = getMinigameFromWorld(event.entity.world) + runningMinigame?.handleEntityDismount(event) + } + + @EventHandler + fun onAnvilDamaged(event: AnvilDamagedEvent) { + event.isCancelled = true + } +} diff --git a/pgame-plugin/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 97% rename from pgame-plugin/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..2a9765f 100644 --- a/pgame-plugin/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,4 +1,4 @@ -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 diff --git a/pgame-plugin/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 pgame-plugin/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/pgame-plugin/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-plugin/src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/PlayerAdminUI.kt similarity index 98% rename from pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt rename to pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/PlayerAdminUI.kt index 5d1b7d6..e60ae4d 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/PlayerAdminUI.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/PlayerAdminUI.kt @@ -1,4 +1,4 @@ -package info.mester.network.partygames.admin +package info.mester.network.partygames.api.admin import com.google.gson.Gson import com.google.gson.annotations.SerializedName 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..0a48d21 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameStartedEvent.kt @@ -0,0 +1,17 @@ +package info.mester.network.partygames.api.events + +import info.mester.network.partygames.api.Game +import org.bukkit.event.Event +import org.bukkit.event.HandlerList + +class GameStartedEvent( + val game: Game, +) : Event() { + companion object { + private val HANDLER_LIST = HandlerList() + + 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..4f6c440 --- /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 +loader: info.mester.network.partygames.Loader +load: POSTWORLD +authors: + - Mester +description: Core API for Party Games +website: https://github.com/MesterNetwork/PartyGames +contributors: + - ChatGPT \ No newline at end of file diff --git a/pgame-plugin/build.gradle.kts b/pgame-plugin/build.gradle.kts index 46db53e..c3b074b 100644 --- a/pgame-plugin/build.gradle.kts +++ b/pgame-plugin/build.gradle.kts @@ -10,26 +10,21 @@ repositories { 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/") + maven("https://repo.extendedclip.com/releases/") } dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation(kotlin("reflect")) - implementation(project(":pgame-api")) + compileOnly(project(":pgame-api")) 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 @@ -80,8 +75,18 @@ tasks { tasks.register("copyPluginToRun") { dependsOn("build") - from(buildDir.resolve("libs").resolve("partygames-${project.version}-all.jar")) - into(rootDir.resolve("run").resolve("plugins")) + 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 { 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 index ff3c084..a1ed975 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt @@ -2,8 +2,8 @@ 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.api.admin.GamesUI +import info.mester.network.partygames.api.admin.InvseeUI import info.mester.network.partygames.game.GameType import info.mester.network.partygames.game.HealthShopMinigame import info.mester.network.partygames.game.SnifferHuntMinigame diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt index 08b0830..9c1ac3a 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt @@ -1,8 +1,6 @@ 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.api.PartyGamesCore import info.mester.network.partygames.game.GameManager import info.mester.network.partygames.level.LevelManager import info.mester.network.partygames.level.LevelPlaceholder @@ -11,12 +9,10 @@ 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 java.io.File @@ -68,17 +64,16 @@ fun Int.pow(power: Int): Double = this.toDouble().pow(power) class PartyGames : JavaPlugin() { lateinit var gameManager: GameManager - lateinit var viaAPI: ViaAPI<*> lateinit var scoreboardLibrary: ScoreboardLibrary lateinit var playingPlaceholder: PlayingPlaceholder lateinit var databaseManager: DatabaseManager lateinit var levelManager: LevelManager lateinit var spawnLocation: Location lateinit var sidebarManager: SidebarManager + lateinit var partyGamesCore: PartyGamesCore companion object { val plugin = PartyGames() - val httpClient = OkHttpClient() fun initWorld(world: World) { world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) @@ -94,46 +89,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,14 +96,6 @@ 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")!! } @@ -166,7 +113,6 @@ 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) 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 index 9f37bd0..d82c4fc 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt @@ -1,8 +1,8 @@ package info.mester.network.partygames 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.api.admin.InvseeUI +import info.mester.network.partygames.api.admin.PlayerAdminUI import info.mester.network.partygames.game.Game import info.mester.network.partygames.game.GameState import info.mester.network.partygames.game.HealthShopMinigame diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/admin/AdminUtils.kt deleted file mode 100644 index 188d290..0000000 --- a/pgame-plugin/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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt index 720e86c..07e3b14 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealer.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 diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt index c32a45f..9c182d1 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt @@ -75,30 +75,6 @@ class GameManager( 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 { ", { - private var rotation = 0.0 - private var lastTime = System.currentTimeMillis() - private var remainingTime = INTRODUCTION_TIME * 20 - - private fun generateProgressBar(): String { - val percentage = (1 - (remainingTime.toDouble() / (INTRODUCTION_TIME * 20))) * 100 - val filledSquares = ((percentage + 5) / 10).toInt().coerceIn(0, 10) - - return buildString { - if (filledSquares > 0) { - append("") - append("■".repeat(filledSquares)) - append("") - } - if (filledSquares < 10) { - append("") - append("■".repeat(10 - filledSquares)) - append("") - } - } - } - - override fun accept(t: BukkitTask) { - val minigame = game.runningMinigame - if (game.state != GameState.PRE_GAME || minigame == null) { - t.cancel() - return - } - 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 - if (rotation > 360.0) { - rotation = 0.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 - // basically, offset the degree by the player's index in the list - val offset = players.indexOf(player) * 360.0 / players.size - val playerRotation = (rotation + offset) % 360.0 - // shoot a line from startpos with the angle and store the hit position - val radians = Math.toRadians(playerRotation) - val hitX = 15.0 * cos(radians) - val hitZ = 15.0 * sin(radians) - // 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 - } - - player.teleportAsync(finalPos) - } - } -} diff --git a/pgame-plugin/src/main/resources/paper-plugin.yml b/pgame-plugin/src/main/resources/paper-plugin.yml index 02aa864..4ef5852 100644 --- a/pgame-plugin/src/main/resources/paper-plugin.yml +++ b/pgame-plugin/src/main/resources/paper-plugin.yml @@ -1,5 +1,5 @@ name: PartyGames -version: 'a1.0' +version: '1.0' main: info.mester.network.partygames.PartyGames api-version: '1.21' bootstrapper: info.mester.network.partygames.Bootstrapper From de9a590055833e28e2e08998b9de23e5764c7ca5 Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Fri, 17 Jan 2025 17:26:58 +0100 Subject: [PATCH 04/18] Finish API --- .../network/partygames/api/Bootstrapper.kt | 149 ++++++++ .../mester/network/partygames/api/Game.kt | 120 +++---- .../network/partygames/api/GameRegistry.kt | 54 ++- .../mester/network/partygames/api/Minigame.kt | 36 +- .../network/partygames/api/PartyGamesCore.kt | 26 +- .../partygames/api/PartyGamesListener.kt | 97 +----- .../network/partygames/api/admin/GamesUI.kt | 15 +- .../partygames/api/admin/PlayerAdminUI.kt | 62 +--- .../partygames/api/events/GameStartedEvent.kt | 5 +- .../api/events/GameTerminatedEvent.kt | 29 ++ pgame-api/src/main/resources/paper-plugin.yml | 4 +- .../mester/network/partygames/Bootstrapper.kt | 127 +------ .../mester/network/partygames/PartyGames.kt | 61 +++- .../network/partygames/PartyListener.kt | 320 ++---------------- ...amageDealer.kt => DamageDealerMinigame.kt} | 2 +- .../network/partygames/game/GameManager.kt | 52 +-- .../partygames/game/GardeningMinigame.kt | 3 + .../partygames/game/HealthShopMinigame.kt | 28 +- .../partygames/game/IntroductionTimer.kt | 3 - .../partygames/game/RunawayMinigame.kt | 86 ----- .../partygames/game/SnifferHuntMinigame.kt | 4 +- .../partygames/game/SpeedBuildersMinigame.kt | 6 +- .../partygames/game/gardening/Cactus.kt | 2 +- .../partygames/game/gardening/Lilac.kt | 2 +- .../partygames/game/gardening/OakTree.kt | 2 +- .../partygames/game/gardening/Peony.kt | 2 +- .../partygames/game/gardening/Plant.kt | 2 +- .../game/gardening/RainbowFlower.kt | 2 +- .../network/partygames/game/gardening/Rose.kt | 2 +- .../partygames/game/gardening/Sunflower.kt | 2 +- .../network/partygames/game/gardening/Weed.kt | 2 +- .../partygames/game/gardening/ZombieWeed.kt | 2 +- .../sidebar/GameSidebarComponent.kt | 4 +- .../partygames/sidebar/SidebarManager.kt | 4 +- pgame-plugin/src/main/resources/config.yml | 6 - .../src/main/resources/paper-plugin.yml | 4 + 36 files changed, 500 insertions(+), 827 deletions(-) create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameTerminatedEvent.kt rename pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/{DamageDealer.kt => DamageDealerMinigame.kt} (99%) delete mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt delete mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt new file mode 100644 index 0000000..6b2e6f1 --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt @@ -0,0 +1,149 @@ +package info.mester.network.partygames.api + +import com.mojang.brigadier.Command +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.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 + }, + ).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/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt index 8da03a8..90448ef 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt @@ -3,6 +3,7 @@ package info.mester.network.partygames.api import com.infernalsuite.aswm.api.AdvancedSlimePaperAPI import com.infernalsuite.aswm.api.world.SlimeWorld import info.mester.network.partygames.api.events.GameStartedEvent +import info.mester.network.partygames.api.events.GameTerminatedEvent import net.kyori.adventure.audience.Audience import net.kyori.adventure.bossbar.BossBar import net.kyori.adventure.key.Key @@ -16,9 +17,6 @@ import org.bukkit.OfflinePlayer import org.bukkit.entity.Player import java.util.UUID import java.util.logging.Level -import kotlin.math.floor - -private fun UUID.shorten() = this.toString().replace("-", "") private val mm = MiniMessage.miniMessage() @@ -87,7 +85,6 @@ class Game( } private val slimeAPI = AdvancedSlimePaperAPI.instance() - private val sidebarManager = plugin.sidebarManager /** * The unique ID of the game @@ -191,8 +188,8 @@ class Game( handleDisconnect(player, true) if (player.isOnline) { resetPlayer(player) - sidebarManager.openLobbySidebar(player) - player.teleport(plugin.spawnLocation) // TODO: replace with PlayerRemovedFromGameEvent + // sidebarManager.openLobbySidebar(player) + // player.teleport(plugin.spawnLocation) // TODO: replace with PlayerRemovedFromGameEvent } if (playerDatas.isEmpty()) { end() @@ -221,25 +218,13 @@ class Game( .first() .call(this) }.toTypedArray() - // update the playing placeholder - plugin.playingPlaceholder.addPlaying(type.name, players.size) // TODO: use the event try { val success = nextMinigame() if (!success) { throw IllegalStateException("Couldn't load the first minigame!") } - val event = GameStartedEvent(this) + val event = GameStartedEvent(this, players) event.callEvent() - // wait a tick and set up the sidebar - Bukkit.getScheduler().runTaskLater( - plugin, - Runnable { - for (player in players) { - plugin.sidebarManager.openGameSidebar(player) - } - }, - 1, - ) } catch (err: IllegalStateException) { // uh-oh! core.logger.log(Level.SEVERE, "An error occurred while setting up the game!", err) @@ -375,22 +360,21 @@ class Game( */ fun terminate() { _state = GameState.STOPPED - plugin.playingPlaceholder.removePlaying(type.name, playerDatas.size) // TODO: replace with GameTerminatedEvent // 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) } /** @@ -429,56 +413,56 @@ class Game( } 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) // TODO: replace with GameEndedEvent - 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)) - } +// for ((player, data) in topList) { +// val oldLevel = plugin.levelManager.levelDataOf(player.uniqueId) // TODO: replace with GameEndedEvent +// 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)) +// } // 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) // TODO: replace with PlayerRejoinedEvent + // plugin.sidebarManager.openGameSidebar(player) // TODO: replace with PlayerRejoinedEvent 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 index 8e74541..10d1b04 100644 --- 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 @@ -1,17 +1,22 @@ 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, +) + data class RegisteredMinigame( val plugin: JavaPlugin, val minigame: KClass, val name: String, - val showInJoin: Boolean, - val startPos: Vector, + val worlds: List, ) data class MinigameBundle( @@ -30,16 +35,21 @@ class GameRegistry( fun registerMinigame( plugin: JavaPlugin, - minigame: KClass, + className: String, name: String, - startPos: Vector, - showInJoin: Boolean, - register: String?, + worlds: List, + registerAs: String? = null, ) { - val registeredMinigame = RegisteredMinigame(plugin, minigame, name, showInJoin, startPos) + 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 (register != null) { - bundles.add(MinigameBundle(plugin, listOf(name), name, register)) + if (registerAs != null) { + bundles.add(MinigameBundle(plugin, listOf(name), name.uppercase(), registerAs)) } } @@ -49,15 +59,10 @@ class GameRegistry( name: String, displayName: String, ) { - bundles.add(MinigameBundle(plugin, minigames, name, displayName)) - } - - fun getStartPos(name: String): Vector? { - val minigame = minigames.firstOrNull { it.name == name } - return minigame?.startPos + bundles.add(MinigameBundle(plugin, minigames, name.uppercase(), displayName)) } - fun getMinigame(name: String): RegisteredMinigame? = minigames.firstOrNull { it.name == name } + fun getMinigame(name: String): RegisteredMinigame? = minigames.firstOrNull { it.name == name.uppercase() } fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name } @@ -65,8 +70,23 @@ class GameRegistry( players: List, bundleName: String, ) { - val bundle = getBundle(bundleName) ?: throw IllegalArgumentException("Bundle $bundleName not found!") + val bundle = + getBundle(bundleName.uppercase()) ?: 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/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index ad1ee9c..77c4e6b 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -1,5 +1,6 @@ package info.mester.network.partygames.api +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 @@ -16,14 +17,19 @@ import org.bukkit.event.entity.EntityChangeBlockEvent import org.bukkit.event.entity.EntityCombustEvent import org.bukkit.event.entity.EntityDamageByEntityEvent 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.scheduler.BukkitTask import java.util.UUID import java.util.function.Consumer @@ -31,7 +37,7 @@ import kotlin.random.Random abstract class Minigame( protected val game: Game, - startPosPath: String, + name: String, ) { private var _running = false private var countdownUUID = UUID.randomUUID() @@ -49,15 +55,11 @@ abstract class Minigame( val worldIndex: Int 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(name)!! + worldIndex = Random.nextInt(0, minigameConfig.worlds.size) + rootWorldName = minigameConfig.worlds[worldIndex].name + startPos = minigameConfig.worlds[worldIndex].startPos.toLocation(Bukkit.getWorld(rootWorldName)!!) } /** @@ -218,6 +220,20 @@ abstract class Minigame( open fun handleEntityDamageByEntity(event: EntityDamageByEntityEvent) {} + open fun handleInventoryClick( + event: InventoryClickEvent, + clickedInventory: Inventory, + ) { + } + + open fun handleEntityShootBow(event: EntityShootBowEvent) {} + + open fun handleBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) {} + + open fun handlePlayerItemConsume(event: PlayerItemConsumeEvent) {} + + open fun handleEntityRegainHealth(event: EntityRegainHealthEvent) {} + 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 index f03f2ae..f138705 100644 --- 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 @@ -1,11 +1,30 @@ 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 @@ -86,6 +105,11 @@ class PartyGamesCore : JavaPlugin() { override fun onEnable() { instance = this - gameRegistry = GameRegistry() + gameRegistry = GameRegistry(this) + Bukkit.getPluginManager().registerEvents(PartyGamesListener(this), this) + } + + override fun onDisable() { + gameRegistry.shutdown() } } diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt index 4b1440d..b09fa81 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt @@ -7,7 +7,6 @@ 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 @@ -43,7 +42,6 @@ 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 class PartyGamesListener( @@ -92,11 +90,6 @@ class PartyGamesListener( holder.onInventoryClick(event) } - if (holder is HealthShopUI) { - event.isCancelled = true - holder.onInventoryClick(event) - } - if (holder is InvseeUI) { event.isCancelled = true return @@ -127,44 +120,9 @@ class PartyGamesListener( @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 = 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") - } - // 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) } @@ -172,7 +130,7 @@ class PartyGamesListener( @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 @@ -183,7 +141,7 @@ class PartyGamesListener( @EventHandler fun onInventoryClose(event: InventoryCloseEvent) { - if (plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } val minigame = getMinigameFromWorld(event.player.world) @@ -192,7 +150,7 @@ class PartyGamesListener( @EventHandler fun onPlayerMove(event: PlayerMoveEvent) { - if (plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } val minigame = getMinigameFromWorld(event.player.world) @@ -201,20 +159,16 @@ class PartyGamesListener( @EventHandler fun onPlayerQuit(event: PlayerQuitEvent) { - plugin.setAdmin(event.player, false) - gameRegistry.getQueueOf(event.player)?.removePlayer(event.player) + core.setAdmin(event.player, false) gameRegistry.getGameOf(event.player)?.handleDisconnect(event.player, false) - plugin.sidebarManager.unregisterPlayer(event.player) } @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)!! @@ -226,7 +180,7 @@ class PartyGamesListener( @EventHandler fun onBlockBreak(event: BlockBreakEvent) { - if (plugin.isAdmin(event.player)) { + if (core.isAdmin(event.player)) { return } val minigame = getMinigameFromWorld(event.player.world) ?: return @@ -236,16 +190,14 @@ class PartyGamesListener( @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 @@ -263,14 +215,12 @@ class PartyGamesListener( @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 } @@ -281,18 +231,16 @@ class PartyGamesListener( @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) @@ -307,7 +255,7 @@ class PartyGamesListener( @EventHandler fun onEntityShootBow(event: EntityShootBowEvent) { - if (plugin.isAdmin(event.entity)) { + if (core.isAdmin(event.entity)) { return } // don't allow arrows from being picked up @@ -316,17 +264,14 @@ class PartyGamesListener( 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) } @@ -348,12 +293,6 @@ class PartyGamesListener( minigame?.handleEntityCombust(event) } - @EventHandler - fun onWorldLoad(event: WorldLoadEvent) { - val world = event.world - PartyGames.initWorld(world) - } - @EventHandler fun onPlayerSwapHandItems(event: PlayerSwapHandItemsEvent) { event.isCancelled = true diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/GamesUI.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/admin/GamesUI.kt index 2a9765f..2bc3887 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/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.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/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 index e60ae4d..5c5d1d5 100644 --- 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 @@ -1,13 +1,9 @@ package info.mester.network.partygames.api.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 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 okhttp3.Request import org.bukkit.Bukkit import org.bukkit.Material import org.bukkit.entity.Player @@ -16,25 +12,6 @@ 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, @@ -81,41 +58,6 @@ class PlayerAdminUI( 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, - ), - ) - } - } + event.whoClicked.sendMessage(Component.text("TODO")) } } 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 index 0a48d21..5b74da7 100644 --- 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 @@ -1,16 +1,19 @@ 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() - fun getHandlerList() = HANDLER_LIST + @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/resources/paper-plugin.yml b/pgame-api/src/main/resources/paper-plugin.yml index 4f6c440..0b7e2e0 100644 --- a/pgame-api/src/main/resources/paper-plugin.yml +++ b/pgame-api/src/main/resources/paper-plugin.yml @@ -3,11 +3,11 @@ version: '1.0' main: info.mester.network.partygames.api.PartyGamesCore api-version: '1.21' bootstrapper: info.mester.network.partygames.api.Bootstrapper -loader: info.mester.network.partygames.Loader load: POSTWORLD authors: - Mester description: Core API for Party Games website: https://github.com/MesterNetwork/PartyGames contributors: - - ChatGPT \ No newline at end of file + - ChatGPT + - Methamphetamine \ No newline at end of file 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 index a1ed975..206576a 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt @@ -2,15 +2,11 @@ package info.mester.network.partygames 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 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 @@ -31,44 +27,12 @@ class Bootstrapper : PluginBootstrap { val manager: LifecycleEventManager = context.lifecycleManager manager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> val commands = event.registrar() - // /admin + // /partygames 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( + .literal("partygames") + .requires { it.sender.hasPermission("partygames.admin") } + .then( // reload Commands .literal("reload") @@ -82,46 +46,7 @@ class Bootstrapper : PluginBootstrap { 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( @@ -173,50 +98,8 @@ class Bootstrapper : PluginBootstrap { }, ).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 + override fun createPlugin(context: PluginProviderContext): JavaPlugin = PartyGames() } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt index 9c1ac3a..6122cbb 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt @@ -1,7 +1,12 @@ package info.mester.network.partygames +import info.mester.network.partygames.api.MinigameWorld import info.mester.network.partygames.api.PartyGamesCore +import info.mester.network.partygames.game.DamageDealer import info.mester.network.partygames.game.GameManager +import info.mester.network.partygames.game.GardeningMinigame +import info.mester.network.partygames.game.HealthShopMinigame +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.sidebar.SidebarManager @@ -63,6 +68,7 @@ fun Int.toRomanNumeral(): String { fun Int.pow(power: Int): Double = this.toDouble().pow(power) class PartyGames : JavaPlugin() { + lateinit var core: PartyGamesCore lateinit var gameManager: GameManager lateinit var scoreboardLibrary: ScoreboardLibrary lateinit var playingPlaceholder: PlayingPlaceholder @@ -70,10 +76,10 @@ class PartyGames : JavaPlugin() { lateinit var levelManager: LevelManager lateinit var spawnLocation: Location lateinit var sidebarManager: SidebarManager - lateinit var partyGamesCore: PartyGamesCore companion object { - val plugin = PartyGames() + private var _plugin: PartyGames? = null + val plugin get() = _plugin!! fun initWorld(world: World) { world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false) @@ -101,6 +107,8 @@ class PartyGames : JavaPlugin() { } override fun onEnable() { + _plugin = this + core = PartyGamesCore.getInstance() saveResource("config.yml", true) saveResource("health-shop.yml", true) saveResource("speed-builders.yml", true) @@ -122,6 +130,7 @@ class PartyGames : JavaPlugin() { playingPlaceholder = PlayingPlaceholder() playingPlaceholder.register() LevelPlaceholder(levelManager).register() + registerMinigames() // set up event listeners server.pluginManager.registerEvents(PartyListener(this), this) // init all worlds @@ -130,9 +139,55 @@ class PartyGames : JavaPlugin() { } } + private fun registerMinigames() { + core.gameRegistry.registerMinigame( + this, + HealthShopMinigame::class.qualifiedName!!, + "health_shop", + listOf( + MinigameWorld("mg-healthshop", org.bukkit.util.Vector(0.5, 63.0, 0.5)), + MinigameWorld("mg-healthshop2", org.bukkit.util.Vector(0.5, 65.0, 0.5)), + ), + "Health Shop", + ) + core.gameRegistry.registerMinigame( + this, + SpeedBuildersMinigame::class.qualifiedName!!, + "speed_builders", + listOf( + MinigameWorld("mg-speedbuilders", org.bukkit.util.Vector(0.5, 60.0, 0.5)), + ), + "Speed Builders", + ) + core.gameRegistry.registerMinigame( + this, + GardeningMinigame::class.qualifiedName!!, + "gardening", + listOf( + MinigameWorld("mg-gardening", org.bukkit.util.Vector(0.5, 65.0, 0.5)), + ), + "Gardening", + ) + core.gameRegistry.registerMinigame( + this, + DamageDealer::class.qualifiedName!!, + "damage_dealer", + listOf( + MinigameWorld("mg-damagedealer", org.bukkit.util.Vector(0.5, 62.0, 0.5)), + ), + "Damage Dealer", + ) + // register bundles for each minigame, then family night + core.gameRegistry.registerBundle( + this, + listOf("healthshop", "speedbuilders", "gardening", "damagedealer"), + "family_night", + "Family Night", + ) + } + override fun onDisable() { // Plugin shutdown logic - gameManager.shutdown() 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 index d82c4fc..5adebbe 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt @@ -1,136 +1,29 @@ package info.mester.network.partygames -import com.destroystokyo.paper.event.block.AnvilDamagedEvent -import info.mester.network.partygames.api.admin.InvseeUI -import info.mester.network.partygames.api.admin.PlayerAdminUI -import info.mester.network.partygames.game.Game -import info.mester.network.partygames.game.GameState +import info.mester.network.partygames.api.GameState +import info.mester.network.partygames.api.events.GameStartedEvent +import info.mester.network.partygames.api.events.GameTerminatedEvent import info.mester.network.partygames.game.HealthShopMinigame -import info.mester.network.partygames.game.SpeedBuildersMinigame -import info.mester.network.partygames.game.healthshop.HealthShopUI -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 -import org.bukkit.entity.AbstractArrow -import org.bukkit.entity.Arrow -import org.bukkit.entity.EntityType -import org.bukkit.entity.Player 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.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.FoodLevelChangeEvent -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.inventory.InventoryType -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.PlayerJoinEvent -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 class PartyListener( private val plugin: PartyGames, ) : Listener { private val gameManager = plugin.gameManager - - private fun getMinigameFromWorld(world: World) = gameManager.getGameByWorld(world)?.runningMinigame - - @EventHandler - fun onPlayerInteractAtEntity(event: PlayerInteractAtEntityEvent) { - if (event.hand == EquipmentSlot.OFF_HAND) { - 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) && - event.rightClicked is Player && - game != null - ) { - event.isCancelled = true - // setup admin ui - val playerAdminUI = PlayerAdminUI(game, event.rightClicked as Player) - event.player.openInventory(playerAdminUI.inventory) - return - } - // execute the event on the minigame - val minigame = getMinigameFromWorld(event.player.world) - minigame?.handlePlayerInteractAtEntity(event) - } - - @EventHandler - fun onInventoryClick(event: InventoryClickEvent) { - val clickedInventory = event.clickedInventory ?: return - val holder = clickedInventory.getHolder(false) - if (holder is Player) { - // don't let players interact with their armor and offhand - if (event.slotType == InventoryType.SlotType.ARMOR || event.slot == 40) { - event.isCancelled = true - return - } - } - - if (holder is PlayerAdminUI) { - event.isCancelled = true - holder.onInventoryClick(event) - } - - if (holder is HealthShopUI) { - event.isCancelled = true - holder.onInventoryClick(event) - } - - if (holder is InvseeUI) { - event.isCancelled = true - return - } - } - - @EventHandler - fun onEntityDamage(event: EntityDamageEvent) { - // cancel fall damage - if (event.entity.type == EntityType.PLAYER && event.cause == EntityDamageEvent.DamageCause.FALL) { - event.isCancelled = true - return - } - } - - @EventHandler - fun onFoodLevelChange(event: FoodLevelChangeEvent) { - if (event.entity.type == EntityType.PLAYER) { - event.isCancelled = true - val player = event.entity as Player - player.foodLevel = 20 - player.saturation = 0f - player.sendHealthUpdate() - } - } + private val core = plugin.core + private val sidebarManager = plugin.sidebarManager @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 @@ -142,7 +35,7 @@ class PartyListener( } } val plainText = PlainTextComponentSerializer.plainText().serialize(event.message()) - val game = gameManager.getGameOf(event.player) ?: return + val game = core.gameRegistry.getGameOf(event.player) ?: return // special code for saying "fire map" if (game.state == GameState.PRE_GAME && game.runningMinigame is HealthShopMinigame && @@ -168,138 +61,18 @@ class PartyListener( val amount = plainText.substringAfter("lose").toIntOrNull() ?: return game.addScore(event.player, -amount, "admin command") } - val minigame = getMinigameFromWorld(event.player.world) - minigame?.handlePlayerChat(event) - } - - @EventHandler - fun onPrePlayerAttack(event: PrePlayerAttackEntityEvent) { - // by default disable the event for every non-admin player - if (plugin.isAdmin(event.player)) { - return - } - event.isCancelled = true - // however, the minigame may override the cancellation, allowing the event - val minigame = getMinigameFromWorld(event.player.world) ?: return - minigame.handlePrePlayerAttack(event) - } - - @EventHandler - fun onInventoryClose(event: InventoryCloseEvent) { - if (plugin.isAdmin(event.player)) { - return - } - val minigame = getMinigameFromWorld(event.player.world) - minigame?.handleInventoryClose(event) - } - - @EventHandler - fun onPlayerMove(event: PlayerMoveEvent) { - if (plugin.isAdmin(event.player)) { - return - } - val minigame = getMinigameFromWorld(event.player.world) - minigame?.handlePlayerMove(event) } @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) } @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) - } - // 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) - } - - @EventHandler - fun onBlockBreak(event: BlockBreakEvent) { - if (plugin.isAdmin(event.player)) { - return - } - val minigame = getMinigameFromWorld(event.player.world) ?: return - event.isCancelled = true // only cancel the event if we're in a minigame - minigame.handleBlockBreak(event) - } - - @EventHandler - fun onEntityRegainHealth(event: EntityRegainHealthEvent) { - if (plugin.isAdmin(event.entity)) { - return - } - if (event.entityType != EntityType.PLAYER) { - return - } - val runningMinigame = getMinigameFromWorld(event.entity.world) - if (runningMinigame is HealthShopMinigame) { - runningMinigame.handleEntityRegainHealth(event) - } - } - - @EventHandler - fun onEntityDamageByEntity(event: EntityDamageByEntityEvent) { - val minigame = getMinigameFromWorld(event.entity.world) - minigame?.handleEntityDamageByEntity(event) - } - - @EventHandler - fun onPlayerDeath(event: PlayerDeathEvent) { - val minigame = getMinigameFromWorld(event.entity.world) - minigame?.handlePlayerDeath(event) - } - - @EventHandler - fun onPlayerItemConsume(event: PlayerItemConsumeEvent) { - val minigame = getMinigameFromWorld(event.player.world) - if (minigame is HealthShopMinigame) { - minigame.handlePlayerItemConsume(event) - } - } - - @EventHandler - fun onPlayerDropItem(event: PlayerDropItemEvent) { - if (plugin.isAdmin(event.player)) { - return - } - - event.isCancelled = true - val minigame = getMinigameFromWorld(event.player.world) - minigame?.handlePlayerDropItem(event) - } - - @EventHandler - fun onBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) { - if (plugin.isAdmin(event.entity)) { - return - } - val minigame = getMinigameFromWorld(event.entity.world) - if (minigame is SpeedBuildersMinigame) { - minigame.handleBlockBreakProgressUpdate(event) - } - } - - @EventHandler - fun onBlockPlace(event: BlockPlaceEvent) { - if (plugin.isAdmin(event.player)) { - return - } - val minigame = getMinigameFromWorld(event.player.world) - minigame?.handleBlockPlace(event) } @EventHandler @@ -308,47 +81,12 @@ class PartyListener( event.newAmount = 0 } - @EventHandler - fun onEntityShootBow(event: EntityShootBowEvent) { - if (plugin.isAdmin(event.entity)) { - return - } - // don't allow arrows from being picked up - val projectile = event.projectile - if (projectile is Arrow) { - projectile.pickupStatus = AbstractArrow.PickupStatus.CREATIVE_ONLY - } - val minigame = getMinigameFromWorld(event.entity.world) - if (minigame is HealthShopMinigame) { - minigame.handleEntityShootBow(event) - } - } - @EventHandler fun onPlayerInteract(event: PlayerInteractEvent) { - if (PartyGames.plugin.isAdmin(event.player)) { + if (plugin.core.isAdmin(event.player)) { return } plugin.gameManager.getQueueOf(event.player)?.handlePlayerInteract(event) - getMinigameFromWorld(event.player.world)?.handlePlayerInteract(event) - } - - @EventHandler - fun onEntityMove(event: EntityMoveEvent) { - val minigame = getMinigameFromWorld(event.entity.world) - minigame?.handleEntityMove(event) - } - - @EventHandler - fun onBlockPhysics(event: BlockPhysicsEvent) { - val minigame = getMinigameFromWorld(event.block.world) - minigame?.handleBlockPhysics(event) - } - - @EventHandler - fun onEntityCombust(event: EntityCombustEvent) { - val minigame = getMinigameFromWorld(event.entity.world) - minigame?.handleEntityCombust(event) } @EventHandler @@ -358,36 +96,22 @@ class PartyListener( } @EventHandler - fun onPlayerSwapHandItems(event: PlayerSwapHandItemsEvent) { - event.isCancelled = true - } - - @EventHandler - fun onEntityChangeBlock(event: EntityChangeBlockEvent) { - val runningMinigame = getMinigameFromWorld(event.entity.world) - runningMinigame?.handleEntityChangeBlock(event) - } - - @EventHandler - fun onInventoryOpen(event: InventoryOpenEvent) { - val runningMinigame = getMinigameFromWorld(event.player.world) - runningMinigame?.handleInventoryOpen(event) - } - - @EventHandler - fun onPlayerToogleFlight(event: PlayerToggleFlightEvent) { - val runningMinigame = getMinigameFromWorld(event.player.world) - runningMinigame?.handlePlayerToggleFlight(event) - } - - @EventHandler - fun onEntityDismount(event: EntityDismountEvent) { - val runningMinigame = getMinigameFromWorld(event.entity.world) - runningMinigame?.handleEntityDismount(event) + fun onGameStarted(event: GameStartedEvent) { + val game = event.game + plugin.playingPlaceholder.addPlaying(game.bundle.name, event.players.size) + for (player in event.players) { + sidebarManager.openGameSidebar(player) + } } @EventHandler - fun onAnvilDamaged(event: AnvilDamagedEvent) { - event.isCancelled = true + 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) } } diff --git a/pgame-plugin/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 99% rename from pgame-plugin/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 07e3b14..41517ad 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealer.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt @@ -139,7 +139,7 @@ data class DamageDealerItem( class DamageDealer( game: Game, -) : Minigame(game, "damagedealer") { +) : Minigame(game, "damage_dealer") { private val levelItems = mutableListOf() init { diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt index 9c182d1..1a3b932 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt @@ -3,37 +3,27 @@ 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 minigames: List, val displayName: String, ) { - HEALTH_SHOP(listOf(HealthShopMinigame::class), "Health Shop"), - SPEED_BUILDERS(listOf(SpeedBuildersMinigame::class), "Speed Builders"), - GARDENING(listOf(GardeningMinigame::class), "Gardening"), + HEALTH_SHOP(listOf("healthshop"), "Health Shop"), + SPEED_BUILDERS(listOf("speedbuilders"), "Speed Builders"), + GARDENING(listOf("gardening"), "Gardening"), FAMILY_NIGHT( listOf( - HealthShopMinigame::class, - SpeedBuildersMinigame::class, - GardeningMinigame::class, - DamageDealer::class, + "healthshop", + "speedbuilders", + "gardening", + "damagedealer", ), "Family Night", ), - SNIFFER_HUNT( - listOf( - SnifferHuntMinigame::class, - ), - "Sniffer Hunt", - ), DAMAGE_DEALER( - listOf( - DamageDealer::class, - ), + listOf("damagedealer"), "Damage Dealer", ), } @@ -41,10 +31,11 @@ enum class GameType( private val mm = MiniMessage.miniMessage() class GameManager( - private val plugin: PartyGames, + plugin: PartyGames, ) { + private val core = plugin.core + private val gameRegistry = core.gameRegistry private val queues = mutableMapOf() - private val games = mutableMapOf() private fun createQueue( type: GameType, @@ -76,7 +67,7 @@ class GameManager( players: List, ) { // check if there is a player that is already in a game - val playersInGame = players.filter { getGameOf(it) != null } + val playersInGame = players.filter { gameRegistry.getGameOf(it) != null } if (playersInGame.isNotEmpty()) { Audience.audience(playersInGame).sendMessage( mm.deserialize( @@ -95,14 +86,6 @@ class GameManager( 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) } @@ -110,13 +93,6 @@ class GameManager( 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) + gameRegistry.startGame(players, queue.type.name) } } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt index 0ee8496..a0e51cc 100644 --- a/pgame-plugin/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 diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index 8abe49c..13d7784 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -2,6 +2,8 @@ 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.HealthShopUI import info.mester.network.partygames.game.healthshop.SupplyChestTimer @@ -36,12 +38,14 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent 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.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.inventory.ItemStack import org.bukkit.inventory.meta.CompassMeta import org.bukkit.persistence.PersistentDataType @@ -68,7 +72,7 @@ class ShopFailedException( class HealthShopMinigame( game: Game, -) : Minigame(game, "healthshop") { +) : Minigame(game, "health_shop") { companion object { private val shopItems: MutableList = mutableListOf() private val startLocations: MutableMap> = mutableMapOf() @@ -401,7 +405,7 @@ class HealthShopMinigame( ) } - fun handleEntityShootBow(event: EntityShootBowEvent) { + override fun handleEntityShootBow(event: EntityShootBowEvent) { if (state != HealthShopMinigameState.FIGHT) { return } @@ -551,7 +555,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,7 +563,7 @@ class HealthShopMinigame( } } - fun handlePlayerItemConsume(event: PlayerItemConsumeEvent) { + override fun handlePlayerItemConsume(event: PlayerItemConsumeEvent) { if (state != HealthShopMinigameState.FIGHT) { return } @@ -596,10 +600,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( @@ -724,6 +727,17 @@ class HealthShopMinigame( } } + override fun handleInventoryClick( + event: InventoryClickEvent, + clickedInventory: Inventory, + ) { + val holder = clickedInventory.getHolder(false) + if (holder is HealthShopUI) { + event.isCancelled = true + holder.onInventoryClick(event) + } + } + override fun handlePlayerToggleFlight(event: PlayerToggleFlightEvent) { val player = event.player if (player.gameMode == GameMode.CREATIVE) { diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt deleted file mode 100644 index 1f8e647..0000000 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/IntroductionTimer.kt +++ /dev/null @@ -1,3 +0,0 @@ -package info.mester.network.partygames.game - -private const val INTRODUCTION_TIME = 8 diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/RunawayMinigame.kt deleted file mode 100644 index 40632c1..0000000 --- a/pgame-plugin/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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt index 6278234..ab93f49 100644 --- a/pgame-plugin/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 @@ -30,7 +32,7 @@ enum class SnifferHuntState { class SnifferHuntMinigame( game: Game, -) : Minigame(game, "snifferhunt") { +) : Minigame(game, "sniffer_hunt") { companion object { lateinit var config: SnifferHuntConfig private set diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt index 1f49f6b..7ca68f5 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt @@ -3,6 +3,8 @@ 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 @@ -86,7 +88,7 @@ const val AREA_OFFSET = 5 class SpeedBuildersMinigame( game: Game, -) : Minigame(game, "speedbuilders") { +) : Minigame(game, "speed_builders") { companion object { val plugin = PartyGames.plugin private val structures = mutableListOf() @@ -327,7 +329,7 @@ class SpeedBuildersMinigame( } } - fun handleBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) { + override fun handleBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) { if (event.entity !is Player) return if (state != SpeedBuildersState.BUILD) return val player = event.entity as Player diff --git a/pgame-plugin/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 index dad2d15..4452f79 100644 --- a/pgame-plugin/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/pgame-plugin/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 index 994b471..1bfb955 100644 --- a/pgame-plugin/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/pgame-plugin/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 index 7494054..0b255bc 100644 --- a/pgame-plugin/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/pgame-plugin/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 index b773494..f4f7f59 100644 --- a/pgame-plugin/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/pgame-plugin/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 index dab910f..5923802 100644 --- a/pgame-plugin/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 diff --git a/pgame-plugin/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 index 80a0984..fe165ca 100644 --- a/pgame-plugin/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/pgame-plugin/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 index 13ff373..3449c5c 100644 --- a/pgame-plugin/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/pgame-plugin/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 index 347aff3..0fdc2c5 100644 --- a/pgame-plugin/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/pgame-plugin/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 index cf1ad02..90eb4c8 100644 --- a/pgame-plugin/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/pgame-plugin/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 index b442320..f91bdcc 100644 --- a/pgame-plugin/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 diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt index d751a85..5b1ee25 100644 --- a/pgame-plugin/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 diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt index e5b4122..84e2932 100644 --- a/pgame-plugin/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 @@ -117,7 +117,7 @@ class SidebarManager( } 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/pgame-plugin/src/main/resources/config.yml b/pgame-plugin/src/main/resources/config.yml index 7f44f87..b0856c2 100644 --- a/pgame-plugin/src/main/resources/config.yml +++ b/pgame-plugin/src/main/resources/config.yml @@ -1,11 +1,5 @@ locations: minigames: - runaway: - worlds: - - mg-runaway - x: 0.5 - y: 63.0 - z: 0.5 healthshop: worlds: - mg-healthshop diff --git a/pgame-plugin/src/main/resources/paper-plugin.yml b/pgame-plugin/src/main/resources/paper-plugin.yml index 4ef5852..cae3460 100644 --- a/pgame-plugin/src/main/resources/paper-plugin.yml +++ b/pgame-plugin/src/main/resources/paper-plugin.yml @@ -7,6 +7,10 @@ loader: info.mester.network.partygames.Loader load: POSTWORLD dependencies: server: + PartyGamesCore: + load: BEFORE + required: true + join-classpath: true WorldEdit: load: BEFORE required: true From c3f8de21a3ab28bf7fe8a0f58a3ce7f7e36dd0b0 Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Fri, 17 Jan 2025 22:04:51 +0100 Subject: [PATCH 05/18] Add API usage to README --- README.md | 199 +++++++++++++++++- pgame-api/build.gradle.kts | 2 +- .../network/partygames/api/GameRegistry.kt | 5 +- .../mester/network/partygames/api/Minigame.kt | 6 +- pgame-plugin/build.gradle.kts | 3 + settings.gradle.kts | 1 + test-minigame/build.gradle.kts | 44 ++++ .../network/testminigame/MinigamePlugin.kt | 45 ++++ .../testminigame/PlaceBlockMinigame.kt | 35 +++ .../src/main/resources/paper-plugin.yml | 13 ++ 10 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 test-minigame/build.gradle.kts create mode 100644 test-minigame/src/main/kotlin/info/mester/network/testminigame/MinigamePlugin.kt create mode 100644 test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt create mode 100644 test-minigame/src/main/resources/paper-plugin.yml diff --git a/README.md b/README.md index 87de208..6a4f77f 100644 --- a/README.md +++ b/README.md @@ -20,4 +20,201 @@ By itself it does not contain any minigames, they have to be registered by exter ### 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. \ No newline at end of file +minigames for that server and other non-game related logic, including a leveling system. + +## 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. diff --git a/pgame-api/build.gradle.kts b/pgame-api/build.gradle.kts index af91cee..b443429 100644 --- a/pgame-api/build.gradle.kts +++ b/pgame-api/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "info.mester.network.partygames" -version = "a1.0" +version = "1.0" repositories { maven("https://repo.papermc.io/repository/maven-public/") 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 index 10d1b04..2cdbb96 100644 --- 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 @@ -40,6 +40,9 @@ class GameRegistry( 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!") @@ -64,7 +67,7 @@ class GameRegistry( fun getMinigame(name: String): RegisteredMinigame? = minigames.firstOrNull { it.name == name.uppercase() } - fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name } + private fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name } fun startGame( players: List, diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index 77c4e6b..e7ba3a7 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -133,7 +133,7 @@ abstract class Minigame( fun startCountdown( duration: Long, showBar: Boolean, - onEnd: () -> Unit, + onEnd: Runnable, ) { if (showBar) { audience.showBossBar(game.remainingBossBar) @@ -154,7 +154,7 @@ abstract class Minigame( } if (!updateRemainingTime(startTime, duration)) { t.cancel() - onEnd() + onEnd.run() } } }, @@ -165,7 +165,7 @@ abstract class Minigame( fun startCountdown( duration: Long, - onEnd: () -> Unit, + onEnd: Runnable, ) { startCountdown(duration, true, onEnd) } diff --git a/pgame-plugin/build.gradle.kts b/pgame-plugin/build.gradle.kts index c3b074b..8d795d5 100644 --- a/pgame-plugin/build.gradle.kts +++ b/pgame-plugin/build.gradle.kts @@ -4,6 +4,9 @@ plugins { 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/") diff --git a/settings.gradle.kts b/settings.gradle.kts index 539dc4c..b8817c3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,3 +9,4 @@ pluginManagement { include("pgame-api") include("pgame-plugin") +include("test-minigame") diff --git a/test-minigame/build.gradle.kts b/test-minigame/build.gradle.kts new file mode 100644 index 0000000..1341f7c --- /dev/null +++ b/test-minigame/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.github.johnrengelman.shadow") version "8.1.1" + id("io.papermc.paperweight.userdev") version "2.0.0-beta.13" + 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() + } +} 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..ccfa0bb --- /dev/null +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/MinigamePlugin.kt @@ -0,0 +1,45 @@ +package info.mester.network.testminigame + +import com.mojang.brigadier.Command +import info.mester.network.partygames.api.MinigameWorld +import info.mester.network.partygames.api.PartyGamesCore +import io.papermc.paper.command.brigadier.Commands +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents +import org.bukkit.Bukkit +import org.bukkit.entity.Player +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", + ) + // register /start command + @Suppress("UnstableApiUsage") + lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> + val commands = event.registrar() + commands.register( + Commands + .literal("start") + .executes { ctx -> + val sender = ctx.source.sender + if (sender !is Player) { + return@executes 1 + } + val players = Bukkit.getOnlinePlayers().toList() + core.gameRegistry.startGame(players, "place_block") + Command.SINGLE_SUCCESS + }.build(), + ) + } + println("PlaceBlockMinigame enabled!") + } +} 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..e982fd7 --- /dev/null +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt @@ -0,0 +1,35 @@ +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 start() { + super.start() + val world = game.world + world.worldBorder.size = 30.0 + world.worldBorder.center = startPos + + for (player in game.onlinePlayers) { + player.inventory.addItem(ItemStack.of(Material.OBSIDIAN, 64)) + } + + startCountdown(20 * 1000, false) { + end() + } + } + + 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/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 From b60a6e508227ae16e648b8508f356251c0855c67 Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Sat, 18 Jan 2025 18:11:47 +0100 Subject: [PATCH 06/18] Implement more events in the API, extend README, add more example code --- README.md | 38 +++++++-- .../mester/network/partygames/api/Game.kt | 58 +++---------- .../network/partygames/api/GameRegistry.kt | 2 + .../mester/network/partygames/api/Minigame.kt | 51 ++++++------ .../partygames/api/events/GameEndedEvent.kt | 21 +++++ .../api/events/PlayerRejoinedEvent.kt | 30 +++++++ .../api/events/PlayerRemovedFromGameEvent.kt | 20 +++++ pgame-plugin/build.gradle.kts | 2 - .../info/mester/network/partygames/Loader.kt | 25 ------ .../network/partygames/PartyListener.kt | 81 ++++++++++++++++++- .../src/main/resources/paper-plugin.yml | 5 -- test-minigame/build.gradle.kts | 13 +++ .../network/testminigame/JavaMinigame.java | 76 +++++++++++++++++ .../network/testminigame/MinigamePlugin.kt | 47 ++++++++--- .../testminigame/PlaceBlockMinigame.kt | 6 +- .../network/testminigame/SimpleMinigame.kt | 25 ++++++ 16 files changed, 376 insertions(+), 124 deletions(-) create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameEndedEvent.kt create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/PlayerRejoinedEvent.kt create mode 100644 pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/PlayerRemovedFromGameEvent.kt delete mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/Loader.kt create mode 100644 test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java create mode 100644 test-minigame/src/main/kotlin/info/mester/network/testminigame/SimpleMinigame.kt diff --git a/README.md b/README.md index 6a4f77f..641f2bc 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,3 @@ -## Dependencies - -- AdvancedSlimePaper for MC 1.21.4 -- WorldEdit 7.3.10 -- ViaVersion 5.2.1 -- ScoreboardLibrary 2.2.2 -- PlaceholderAPI 2.11.6 - ## Structure The project is divided into two parts: the core API and the plugin for Mester Network. @@ -17,11 +9,20 @@ 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 +- PlaceholderAPI 2.11.6 + ## API Usage ### Including the API as dependency @@ -218,3 +219,24 @@ The `players` parameter is a list of `Player` objects, and the `bundleName` para 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/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt index 90448ef..7b2d58e 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt @@ -2,8 +2,11 @@ package info.mester.network.partygames.api import com.infernalsuite.aswm.api.AdvancedSlimePaperAPI import com.infernalsuite.aswm.api.world.SlimeWorld +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 @@ -188,9 +191,9 @@ class Game( handleDisconnect(player, true) if (player.isOnline) { resetPlayer(player) - // sidebarManager.openLobbySidebar(player) - // player.teleport(plugin.spawnLocation) // TODO: replace with PlayerRemovedFromGameEvent } + val event = PlayerRemovedFromGameEvent(this, player) + event.callEvent() if (playerDatas.isEmpty()) { end() } @@ -412,57 +415,18 @@ class Game( 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) // TODO: replace with GameEndedEvent -// 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) // TODO: replace with PlayerRejoinedEvent + 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 index 2cdbb96..c48a5a5 100644 --- 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 @@ -69,6 +69,8 @@ class GameRegistry( private fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name } + fun getBundles(): List = bundles + fun startGame( players: List, bundleName: String, diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index e7ba3a7..cd2faa3 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -176,38 +176,55 @@ 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 handleEntityDamageByEntity(event: EntityDamageByEntityEvent) {} - open fun handlePlayerDeath(event: PlayerDeathEvent) {} + open fun handleEntityRegainHealth(event: EntityRegainHealthEvent) {} + + open fun handleEntityShootBow(event: EntityShootBowEvent) {} + + // 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) {} + + // game events open fun handleDisconnect( player: Player, didLeave: Boolean, @@ -216,24 +233,12 @@ abstract class Minigame( open fun handleRejoin(player: Player) {} - open fun handlePlayerChat(event: AsyncChatEvent) {} - - open fun handleEntityDamageByEntity(event: EntityDamageByEntityEvent) {} - open fun handleInventoryClick( event: InventoryClickEvent, clickedInventory: Inventory, ) { } - open fun handleEntityShootBow(event: EntityShootBowEvent) {} - - open fun handleBlockBreakProgressUpdate(event: BlockBreakProgressUpdateEvent) {} - - open fun handlePlayerItemConsume(event: PlayerItemConsumeEvent) {} - - open fun handleEntityRegainHealth(event: EntityRegainHealthEvent) {} - abstract val name: Component abstract val description: Component } 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..c2f70bd --- /dev/null +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/events/GameEndedEvent.kt @@ -0,0 +1,21 @@ +package info.mester.network.partygames.api.events + +import info.mester.network.partygames.api.Game +import info.mester.network.partygames.api.PlayerData +import org.bukkit.OfflinePlayer +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/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-plugin/build.gradle.kts b/pgame-plugin/build.gradle.kts index 8d795d5..dc16988 100644 --- a/pgame-plugin/build.gradle.kts +++ b/pgame-plugin/build.gradle.kts @@ -22,8 +22,6 @@ dependencies { compileOnly(project(":pgame-api")) paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") - - compileOnly("net.objecthunter:exp4j:0.4.8") // WorldEdit compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.10-SNAPSHOT") // AdvancedSlimePaper diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Loader.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Loader.kt deleted file mode 100644 index 075e94d..0000000 --- a/pgame-plugin/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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt index 5adebbe..aaea623 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt @@ -1,11 +1,15 @@ 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 io.papermc.paper.event.player.AsyncChatEvent import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer +import org.bukkit.Bukkit import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.entity.ArrowBodyCountChangeEvent @@ -13,6 +17,7 @@ import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.event.world.WorldLoadEvent +import kotlin.math.floor class PartyListener( private val plugin: PartyGames, @@ -99,9 +104,15 @@ class PartyListener( fun onGameStarted(event: GameStartedEvent) { val game = event.game plugin.playingPlaceholder.addPlaying(game.bundle.name, event.players.size) - for (player in event.players) { - sidebarManager.openGameSidebar(player) - } + Bukkit.getScheduler().runTaskLater( + plugin, + Runnable { + for (player in event.players) { + sidebarManager.openGameSidebar(player) + } + }, + 1, + ) } @EventHandler @@ -114,4 +125,68 @@ class PartyListener( } event.setSpawnLocation(plugin.spawnLocation) } + + @EventHandler + fun onGameEnded(event: GameEndedEvent) { + // increase everyone's xp based on the score + for ((player, data) in event.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 onlinePlayer = Bukkit.getPlayer(player.uniqueId) ?: return + 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(30)}") + } + onlinePlayer.sendMessage(mm.deserialize(levelUpMessage)) + } + } + + @EventHandler + fun onPlayerRejoined(event: PlayerRejoinedEvent) { + val player = event.player + plugin.sidebarManager.openGameSidebar(player) + } + + @EventHandler + fun onPlayerRemovedFromGame(event: PlayerRemovedFromGameEvent) { + val player = event.player + if (player.isOnline) { + plugin.sidebarManager.openLobbySidebar(player) + player.teleport(plugin.spawnLocation) + } + } } diff --git a/pgame-plugin/src/main/resources/paper-plugin.yml b/pgame-plugin/src/main/resources/paper-plugin.yml index cae3460..c29cf60 100644 --- a/pgame-plugin/src/main/resources/paper-plugin.yml +++ b/pgame-plugin/src/main/resources/paper-plugin.yml @@ -3,7 +3,6 @@ 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: @@ -15,10 +14,6 @@ dependencies: load: BEFORE required: true join-classpath: true - ViaVersion: - load: BEFORE - required: true - join-classpath: true PlaceholderAPI: load: BEFORE required: true diff --git a/test-minigame/build.gradle.kts b/test-minigame/build.gradle.kts index 1341f7c..7134dff 100644 --- a/test-minigame/build.gradle.kts +++ b/test-minigame/build.gradle.kts @@ -42,3 +42,16 @@ tasks { 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..10c695f --- /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"); + + // 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)); + } + + // create a countdown without the bar on top of the screen + startCountdown(20 * 1000, false, this::end); + } + + // 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) { + // only award points to players + Player killerPlayer = (Player) killer; + 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(); + } + + @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 index ccfa0bb..ecab49e 100644 --- a/test-minigame/src/main/kotlin/info/mester/network/testminigame/MinigamePlugin.kt +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/MinigamePlugin.kt @@ -1,6 +1,7 @@ package info.mester.network.testminigame import com.mojang.brigadier.Command +import com.mojang.brigadier.arguments.StringArgumentType import info.mester.network.partygames.api.MinigameWorld import info.mester.network.partygames.api.PartyGamesCore import io.papermc.paper.command.brigadier.Commands @@ -22,6 +23,24 @@ class MinigamePlugin : JavaPlugin() { ), "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", + ) // register /start command @Suppress("UnstableApiUsage") lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> @@ -29,17 +48,25 @@ class MinigamePlugin : JavaPlugin() { commands.register( Commands .literal("start") - .executes { ctx -> - val sender = ctx.source.sender - if (sender !is Player) { - return@executes 1 - } - val players = Bukkit.getOnlinePlayers().toList() - core.gameRegistry.startGame(players, "place_block") - Command.SINGLE_SUCCESS - }.build(), + .then( + Commands + .argument("bundle", StringArgumentType.word()) + .suggests { _, builder -> + val bundles = core.gameRegistry.getBundles() + bundles.map { it.name }.forEach(builder::suggest) + builder.buildFuture() + }.executes { ctx -> + val sender = ctx.source.sender + if (sender !is Player) { + return@executes 1 + } + val bundleName = StringArgumentType.getString(ctx, "bundle") + val players = Bukkit.getOnlinePlayers().toList() + core.gameRegistry.startGame(players, bundleName) + Command.SINGLE_SUCCESS + }, + ).build(), ) } - println("PlaceBlockMinigame enabled!") } } 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 index e982fd7..83f96c3 100644 --- a/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt @@ -13,11 +13,14 @@ class PlaceBlockMinigame( ) : Minigame(game, "place_block") { override fun start() { super.start() + // access the world using game.world val world = game.world world.worldBorder.size = 30.0 world.worldBorder.center = startPos - for (player in game.onlinePlayers) { + 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)) } @@ -26,6 +29,7 @@ class PlaceBlockMinigame( } } + // override functions to add custom functionality to the minigame override fun handleBlockPlace(event: BlockPlaceEvent) { game.addScore(event.player, 1, "Placed a block") } 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..a167fbf --- /dev/null +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/SimpleMinigame.kt @@ -0,0 +1,25 @@ +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 + startCountdown(20 * 1000) { + 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) +} From c6b0379d238408938ae993d46c5951f9f0212eb9 Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Sat, 18 Jan 2025 18:19:55 +0100 Subject: [PATCH 07/18] Fix broken JavaMinigame --- .../network/testminigame/JavaMinigame.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 index 10c695f..e69e4aa 100644 --- a/test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java @@ -17,17 +17,6 @@ public class JavaMinigame extends Minigame { public JavaMinigame(@NotNull Game game) { super(game, "java"); - - // 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)); - } - - // create a countdown without the bar on top of the screen - startCountdown(20 * 1000, false, this::end); } // to enable players damaging entities, you need to uncancel prePlayerAttackEntity @@ -62,6 +51,17 @@ public void handlePlayerDeath(@NotNull PlayerDeathEvent event) { @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)); + } + + // create a countdown without the bar on top of the screen + startCountdown(20 * 1000, false, this::end); } @Override From 80aee55fd70558b4be4f9fdf9c50ae6a88b43b3d Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Sat, 18 Jan 2025 18:39:31 +0100 Subject: [PATCH 08/18] Replace Kotlin's Pair with data class --- .../kotlin/info/mester/network/partygames/api/Game.kt | 11 ++++++++--- .../network/partygames/api/events/GameEndedEvent.kt | 5 ++--- .../partygames/sidebar/GameSidebarComponent.kt | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt index 7b2d58e..cc6e557 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt @@ -27,6 +27,11 @@ data class PlayerData( var score: Int, ) +data class TopPlayerData( + val player: OfflinePlayer, + val data: PlayerData, +) + enum class GameState { /** * The game is in the loading state, where the players fly around the starting position and the game is explained @@ -161,12 +166,12 @@ class Game( * @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 } + .map { TopPlayerData(Bukkit.getOfflinePlayer(it.first), it.second) } fun topPlayers() = topPlayers(playerDatas.size) @@ -408,7 +413,7 @@ class Game( "" } append( - "${color}${i + 1}. ${topPlayer?.first?.name ?: "Nobody"} - ${topPlayer?.second?.score ?: 0}\n", + "${color}${i + 1}. ${topPlayer?.player?.name ?: "Nobody"} - ${topPlayer?.data?.score ?: 0}\n", ) } 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 index c2f70bd..23a477a 100644 --- 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 @@ -1,14 +1,13 @@ package info.mester.network.partygames.api.events import info.mester.network.partygames.api.Game -import info.mester.network.partygames.api.PlayerData -import org.bukkit.OfflinePlayer +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>, + val topList: List, ) : Event() { companion object { private val HANDLER_LIST = HandlerList() diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt index 5b1ee25..9706cba 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt @@ -22,8 +22,8 @@ class GameSidebarComponent( break } val data = topList[i] - val player = data.first - val playerData = data.second + val player = data.player + val playerData = data.data drawable.drawLine( mm.deserialize( // display the player's name in gray if they're offline From 23b47bc17c3ab7094be7ce9236422d3ec8fd1fae Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Sat, 18 Jan 2025 18:41:30 +0100 Subject: [PATCH 09/18] small improvement in JavaMinigame --- .../kotlin/info/mester/network/testminigame/JavaMinigame.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index e69e4aa..7ec39cf 100644 --- a/test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java @@ -33,9 +33,8 @@ public void handlePlayerDeath(@NotNull PlayerDeathEvent event) { player.setGameMode(GameMode.SPECTATOR); @SuppressWarnings("UnstableApiUsage") Entity killer = event.getDamageSource().getCausingEntity(); - if (killer instanceof Player) { + if (killer instanceof Player killerPlayer) { // only award points to players - Player killerPlayer = (Player) killer; killerPlayer.sendMessage(Component.text("You killed " + player.getName() + "!")); getGame().addScore(killerPlayer, 10, "Killed " + player.getName()); } From 790ab369a496c624b40d112523ae6dd45ad0ca85 Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Wed, 22 Jan 2025 20:01:31 +0100 Subject: [PATCH 10/18] Finish separation, add Mineguessr + Added /admin start + New Game.startTime property + Added onLoad() to Minigame Health Shop: ~ increase oak plans from 24 to 64 ~ make supply chests rarer Gardening: make higher power shoot even farther Add Mineguessr minigame Speed Builders: add structure name text Level system: 15 XP per 30 seconds played --- .../network/partygames/api/Bootstrapper.kt | 30 +++ .../mester/network/partygames/api/Game.kt | 6 + .../network/partygames/api/GameRegistry.kt | 10 +- .../mester/network/partygames/api/Minigame.kt | 11 +- .../partygames/api/PartyGamesListener.kt | 10 + pgame-plugin/build.gradle.kts | 6 +- pgame-plugin/src/config-schema.json | 108 ++++---- .../mester/network/partygames/Bootstrapper.kt | 24 +- .../mester/network/partygames/PartyGames.kt | 83 +++--- .../network/partygames/PartyListener.kt | 19 +- .../partygames/game/DamageDealerMinigame.kt | 10 +- .../network/partygames/game/GameManager.kt | 23 +- .../partygames/game/GardeningMinigame.kt | 16 +- .../partygames/game/HealthShopMinigame.kt | 13 +- .../partygames/game/MineguessrMinigame.kt | 252 ++++++++++++++++++ .../partygames/game/SnifferHuntMinigame.kt | 2 +- .../partygames/game/SpeedBuildersMinigame.kt | 18 +- .../game/healthshop/HealthShopUI.kt | 2 +- .../game/healthshop/SupplyChestTimer.kt | 6 +- pgame-plugin/src/main/resources/config.yml | 94 ++++--- .../src/main/resources/paper-plugin.yml | 2 +- .../src/main/resources/speed-builders.yml | 18 ++ pgame-plugin/src/speed-builders-schema.json | 7 +- .../network/testminigame/JavaMinigame.java | 1 + .../network/testminigame/MinigamePlugin.kt | 33 --- .../testminigame/PlaceBlockMinigame.kt | 7 +- 26 files changed, 584 insertions(+), 227 deletions(-) create mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/MineguessrMinigame.kt diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt index 6b2e6f1..d23e2f8 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt @@ -1,6 +1,7 @@ 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 @@ -14,6 +15,7 @@ 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 @@ -81,6 +83,34 @@ class Bootstrapper : PluginBootstrap { 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 + }, + ), ).build(), "Main function for managing tournaments", ) diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt index cc6e557..c61b757 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt @@ -132,6 +132,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 */ @@ -277,6 +282,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) 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 index c48a5a5..995a9f8 100644 --- 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 @@ -65,9 +65,14 @@ class GameRegistry( 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() } - private fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name } + private fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name.uppercase() } fun getBundles(): List = bundles @@ -75,8 +80,7 @@ class GameRegistry( players: List, bundleName: String, ) { - val bundle = - getBundle(bundleName.uppercase()) ?: throw IllegalArgumentException("Bundle $bundleName not found!") + val bundle = getBundle(bundleName) ?: throw IllegalArgumentException("Bundle $bundleName not found!") val game = Game(core, bundle, players) games[game.id] = game } diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index cd2faa3..df5a1a3 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -30,6 +30,7 @@ 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 @@ -53,10 +54,12 @@ abstract class Minigame( } val rootWorldName: String val worldIndex: Int + val originalPlugin: JavaPlugin init { val core = PartyGamesCore.getInstance() val minigameConfig = core.gameRegistry.getMinigame(name)!! + originalPlugin = minigameConfig.plugin worldIndex = Random.nextInt(0, minigameConfig.worlds.size) rootWorldName = minigameConfig.worlds[worldIndex].name startPos = minigameConfig.worlds[worldIndex].startPos.toLocation(Bukkit.getWorld(rootWorldName)!!) @@ -74,6 +77,12 @@ 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() {} + /** * 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 @@ -81,7 +90,7 @@ abstract class Minigame( 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 diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt index b09fa81..4c39c56 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt @@ -38,6 +38,7 @@ 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 @@ -326,4 +327,13 @@ class PartyGamesListener( 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 + } + } } diff --git a/pgame-plugin/build.gradle.kts b/pgame-plugin/build.gradle.kts index dc16988..755da80 100644 --- a/pgame-plugin/build.gradle.kts +++ b/pgame-plugin/build.gradle.kts @@ -22,8 +22,10 @@ dependencies { compileOnly(project(":pgame-api")) paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") - // WorldEdit - compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.10-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 diff --git a/pgame-plugin/src/config-schema.json b/pgame-plugin/src/config-schema.json index 35995ed..33e7822 100644 --- a/pgame-plugin/src/config-schema.json +++ b/pgame-plugin/src/config-schema.json @@ -2,60 +2,53 @@ "$schema": "https://json-schema.org/draft-07/schema", "type": "object", "properties": { - "locations": { + "minigames": { "type": "object", - "properties": { - "minigames": { + "patternProperties": { + "^[a-zA-Z0-9_]+$": { "type": "object", - "patternProperties": { - "^[a-zA-Z0-9_]+$": { - "type": "object", - "properties": { - "worlds": { - "type": "array", - "items": { + "properties": { + "worlds": { + "type": "array", + "items": { + "type": "object", + "properties": { + "world": { "type": "string" + }, + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" } }, - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "z": { - "type": "number" - } - }, - "required": [ - "worlds", - "x", - "y", - "z" - ] + "required": [ + "world", + "x", + "y", + "z" + ], + "additionalProperties": false + } + }, + "class": { + "type": "string" + }, + "display-name": { + "type": "string" } }, + "required": [ + "worlds", + "class" + ], "additionalProperties": false } - }, - "required": [ - "minigames" - ] - }, - "steve-skin": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "signature": { - "type": "string" - } - }, - "required": [ - "value", - "signature" - ] + } }, "save-interval": { "type": "integer", @@ -91,13 +84,32 @@ "yaw", "pitch" ] + }, + "mineguessr": { + "type": "object", + "properties": { + "world": { + "type": "string" + }, + "max-size": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "world", + "max-size" + ] + }, + "replace-config": { + "type": "boolean" } }, "required": [ - "locations", - "steve-skin", + "minigames", "save-interval", - "spawn-location" + "spawn-location", + "mineguessr" ], "additionalProperties": false } \ No newline at end of file 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 index 206576a..2a1b3fc 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt @@ -2,8 +2,10 @@ 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.GameType import info.mester.network.partygames.game.HealthShopMinigame +import info.mester.network.partygames.game.MineguessrMinigame import info.mester.network.partygames.game.SnifferHuntMinigame import info.mester.network.partygames.game.SpeedBuildersMinigame import io.papermc.paper.command.brigadier.Commands @@ -38,11 +40,11 @@ class Bootstrapper : PluginBootstrap { .literal("reload") .executes { ctx -> val sender = ctx.source.sender + PartyGames.plugin.reload() HealthShopMinigame.reload() SpeedBuildersMinigame.reload() SnifferHuntMinigame.reload() - PartyGames.plugin.reloadConfig() - PartyGames.plugin.reload() + MineguessrMinigame.reload() sender.sendMessage(Component.text("Reloaded the configuration!", NamedTextColor.GREEN)) Command.SINGLE_SUCCESS }, @@ -56,15 +58,23 @@ class Bootstrapper : PluginBootstrap { 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, "game").uppercase() - for (game in GameType.entries.filter { it.name.uppercase().startsWith(type) }) { - builder.suggest(game.name.lowercase()) + val type = StringArgumentType.getString(ctx.child, "game").lowercase() + for (game in bundles.filter { it.startsWith(type) }) { + builder.suggest(game.lowercase()) } }.onFailure { - for (game in GameType.entries) { - builder.suggest(game.name.lowercase()) + for (game in bundles) { + builder.suggest(game.lowercase()) } } builder.buildFuture() diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt index 6122cbb..cb46ebf 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt @@ -2,11 +2,7 @@ package info.mester.network.partygames import info.mester.network.partygames.api.MinigameWorld import info.mester.network.partygames.api.PartyGamesCore -import info.mester.network.partygames.game.DamageDealer import info.mester.network.partygames.game.GameManager -import info.mester.network.partygames.game.GardeningMinigame -import info.mester.network.partygames.game.HealthShopMinigame -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.sidebar.SidebarManager @@ -103,17 +99,19 @@ class PartyGames : JavaPlugin() { } fun reload() { + reloadConfig() spawnLocation = config.getLocation("spawn-location")!! + registerMinigames() } override fun onEnable() { _plugin = this core = PartyGamesCore.getInstance() - saveResource("config.yml", true) - saveResource("health-shop.yml", true) - saveResource("speed-builders.yml", true) - saveResource("sniffer-hunt.yml", true) - spawnLocation = config.getLocation("spawn-location")!! + saveResource("config.yml", false) + saveResource("health-shop.yml", false) + saveResource("speed-builders.yml", false) + saveResource("sniffer-hunt.yml", false) + reload() // register low-level APIs try { scoreboardLibrary = ScoreboardLibrary.loadScoreboardLibrary(this) @@ -130,7 +128,6 @@ class PartyGames : JavaPlugin() { playingPlaceholder = PlayingPlaceholder() playingPlaceholder.register() LevelPlaceholder(levelManager).register() - registerMinigames() // set up event listeners server.pluginManager.registerEvents(PartyListener(this), this) // init all worlds @@ -140,48 +137,38 @@ class PartyGames : JavaPlugin() { } private fun registerMinigames() { - core.gameRegistry.registerMinigame( - this, - HealthShopMinigame::class.qualifiedName!!, - "health_shop", - listOf( - MinigameWorld("mg-healthshop", org.bukkit.util.Vector(0.5, 63.0, 0.5)), - MinigameWorld("mg-healthshop2", org.bukkit.util.Vector(0.5, 65.0, 0.5)), - ), - "Health Shop", - ) - core.gameRegistry.registerMinigame( - this, - SpeedBuildersMinigame::class.qualifiedName!!, - "speed_builders", - listOf( - MinigameWorld("mg-speedbuilders", org.bukkit.util.Vector(0.5, 60.0, 0.5)), - ), - "Speed Builders", - ) - core.gameRegistry.registerMinigame( - this, - GardeningMinigame::class.qualifiedName!!, - "gardening", - listOf( - MinigameWorld("mg-gardening", org.bukkit.util.Vector(0.5, 65.0, 0.5)), - ), - "Gardening", - ) - core.gameRegistry.registerMinigame( - this, - DamageDealer::class.qualifiedName!!, - "damage_dealer", - listOf( - MinigameWorld("mg-damagedealer", org.bukkit.util.Vector(0.5, 62.0, 0.5)), - ), - "Damage Dealer", - ) + 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 + MinigameWorld(world, org.bukkit.util.Vector(x, y, z)) + } 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, listOf("healthshop", "speedbuilders", "gardening", "damagedealer"), - "family_night", + "familynight", "Family Night", ) } 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 index aaea623..ec65a62 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt @@ -128,10 +128,14 @@ class PartyListener( @EventHandler fun onGameEnded(event: GameEndedEvent) { + val timeElapsed = (Bukkit.getCurrentTick() - event.game.startTime) * 0.05 // increase everyone's xp based on the score for ((player, data) in event.topList) { val oldLevel = plugin.levelManager.levelDataOf(player.uniqueId) - plugin.levelManager.addXp(player.uniqueId, data.score.coerceAtLeast(0)) + // XP is points gained + 15 per half a minute + val xpFromPlayTime = (timeElapsed.toInt() / 30) * 15 + val addedXp = data.score.coerceAtLeast(0) + xpFromPlayTime + plugin.levelManager.addXp(player.uniqueId, addedXp) val newLevel = plugin.levelManager.levelDataOf(player.uniqueId) val onlinePlayer = Bukkit.getPlayer(player.uniqueId) ?: return val levelUpMessage = @@ -155,7 +159,7 @@ class PartyListener( // 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) { + if (additionalSquares == 0 && (newLevel.xp - oldLevel.xp) > 0) { previousFilledSquares -= 1 additionalSquares = 1 } @@ -169,6 +173,8 @@ class PartyListener( append("■") } append("] ${newLevel.xpToNextLevel}\n") + append("+${data.score} XP (Points Gained)\n") + append("+$xpFromPlayTime XP (Time Played)\n") append("${"-".repeat(30)}") } onlinePlayer.sendMessage(mm.deserialize(levelUpMessage)) @@ -178,7 +184,13 @@ class PartyListener( @EventHandler fun onPlayerRejoined(event: PlayerRejoinedEvent) { val player = event.player - plugin.sidebarManager.openGameSidebar(player) + Bukkit.getScheduler().runTaskLater( + plugin, + Runnable { + plugin.sidebarManager.openGameSidebar(player) + }, + 1, + ) } @EventHandler @@ -188,5 +200,6 @@ class PartyListener( plugin.sidebarManager.openLobbySidebar(player) player.teleport(plugin.spawnLocation) } + plugin.playingPlaceholder.removePlaying(event.game.bundle.name, 1) } } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt index 41517ad..3519030 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt @@ -137,9 +137,10 @@ data class DamageDealerItem( } } -class DamageDealer( +@Suppress("Unused") +class DamageDealerMinigame( game: Game, -) : Minigame(game, "damage_dealer") { +) : Minigame(game, "damagedealer") { private val levelItems = mutableListOf() init { @@ -176,9 +177,12 @@ class DamageDealer( entity.isCustomNameVisible = true } + override fun onLoad() { + game.world.setGameRule(GameRule.NATURAL_REGENERATION, false) + } + 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) diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt index 1a3b932..e23ad06 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt @@ -7,25 +7,14 @@ import org.bukkit.entity.Player import java.util.UUID enum class GameType( - val minigames: List, val displayName: String, ) { - HEALTH_SHOP(listOf("healthshop"), "Health Shop"), - SPEED_BUILDERS(listOf("speedbuilders"), "Speed Builders"), - GARDENING(listOf("gardening"), "Gardening"), - FAMILY_NIGHT( - listOf( - "healthshop", - "speedbuilders", - "gardening", - "damagedealer", - ), - "Family Night", - ), - DAMAGE_DEALER( - listOf("damagedealer"), - "Damage Dealer", - ), + HEALTHSHOP("Health Shop"), + SPEEDBUILDERS("Speed Builders"), + GARDENING("Gardening"), + FAMILYNIGHT("Family Night"), + DAMAGEDEALER("Damage Dealer"), + MINEGUESSR("Mineguessr"), } private val mm = MiniMessage.miniMessage() diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt index a0e51cc..c79353d 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt @@ -64,6 +64,7 @@ private enum class ObjectType { LILAC, } +@Suppress("Unused") class GardeningMinigame( game: Game, ) : Minigame(game, "gardening") { @@ -88,8 +89,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() @@ -257,13 +258,16 @@ 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 + } + + override fun start() { + super.start() + fetchAllGrassBlocks() // start a timer that triggers the hose shooting Bukkit.getScheduler().runTaskTimer(plugin, { t -> if (!running) { diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index 13d7784..9f31f11 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -72,7 +72,7 @@ class ShopFailedException( class HealthShopMinigame( game: Game, -) : Minigame(game, "health_shop") { +) : Minigame(game, "healthshop") { companion object { private val shopItems: MutableList = mutableListOf() private val startLocations: MutableMap> = mutableMapOf() @@ -225,16 +225,19 @@ class HealthShopMinigame( } } - 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 + } + + 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() 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..d3537c0 --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/MineguessrMinigame.kt @@ -0,0 +1,252 @@ +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 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.UUID +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] +} + +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 = emptyList() + + 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 var selectedBiomeIndex = 0 + + private fun getRandomChunk(): ChunkSnapshot { + 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) + return sourceWorld.getChunkAt(chunkX, chunkZ, true).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() { + audience.sendActionBar(Component.text("Loading chunk...", NamedTextColor.YELLOW)) + biomeList.clear() + val chunk = getRandomChunk() + copyChunk(chunk) + audience.sendMessage( + MiniMessage.miniMessage().deserialize( + "${"-".repeat(30)}\n" + + "Loading finished, time to guess!\n" + + "This chunk contains ${biomeList.size} biomes.", + ), + ) + selectedBiomeIndex = Random.nextInt(0, biomeList.size) + // turn biome text into underscores, "EXAMPLE_BIOME" -> "_______ _____" + val biomeText = + biomeList[selectedBiomeIndex] + .name() + .map { + if (it == '_') { + ' ' + } else { + "_" + } + }.joinToString("") + audience.sendMessage( + MiniMessage.miniMessage().deserialize("Hint for one biome: $biomeText"), + ) + } + + private fun startRound() { + state = MineguessrState.LOADING + loadChunk() + guessed.clear() + state = MineguessrState.GUESSING + startCountdown(15 * 1000) { + finishRound() + } + } + + private fun formatBiomeName(biome: Biome): String = + biome + .name() + .split('_') // Split by underscores + .joinToString(" ") { word -> + word.lowercase().replaceFirstChar { it.uppercase() } // Capitalize first letter + } + + private fun finishRound() { + stopCountdown() + // turn biomes into this: "Biome Name, "Biome Name2", "Biome Name3" + val biomeText = biomeList.joinToString(", ") { formatBiomeName(it) } + audience.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 start() { + super.start() + + game.world.setGameRule(GameRule.REDUCED_DEBUG_INFO, true) + + for (player in game.onlinePlayers) { + player.gameMode = GameMode.SPECTATOR + } + + startRound() + } + + override fun handleBlockPhysics(event: BlockPhysicsEvent) { + event.isCancelled = true + } + + override fun handlePlayerChat(event: AsyncChatEvent) { + 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.name().replace("_", " ").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 -> 15 + 1 -> 10 + 2 -> 5 + else -> 3 + } + 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)) + event.isCancelled = true + } + } + + 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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt index ab93f49..0ac7f3a 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt @@ -32,7 +32,7 @@ enum class SnifferHuntState { class SnifferHuntMinigame( game: Game, -) : Minigame(game, "sniffer_hunt") { +) : Minigame(game, "snifferhunt") { companion object { lateinit var config: SnifferHuntConfig private set diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt index 7ca68f5..043be7d 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt @@ -13,6 +13,7 @@ import io.papermc.paper.event.block.BlockBreakProgressUpdateEvent import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.title.Title +import net.kyori.adventure.title.TitlePart import org.bukkit.Bukkit import org.bukkit.GameMode import org.bukkit.Location @@ -64,6 +65,7 @@ enum class SpeedBuildersState { data class StructureData( val name: String, val difficulty: StructureDifficulty, + val displayName: String, ) { val fileName = "${name.lowercase()}.nbt" } @@ -88,7 +90,7 @@ const val AREA_OFFSET = 5 class SpeedBuildersMinigame( game: Game, -) : Minigame(game, "speed_builders") { +) : Minigame(game, "speedbuilders") { companion object { val plugin = PartyGames.plugin private val structures = mutableListOf() @@ -102,7 +104,8 @@ 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")!! + structures.add(StructureData(key, StructureDifficulty.valueOf(difficulty.uppercase()), displayName)) } catch (e: Exception) { plugin.logger.warning("Failed to load structure $key") plugin.logger.log(Level.WARNING, e.message, e) @@ -138,7 +141,7 @@ class SpeedBuildersMinigame( } val structureData = currentStructureData!! val structureFile = structureData.fileName - return structureManager.loadStructure(File(plugin.dataFolder, "speedbuilders/$structureFile")) + return structureManager.loadStructure(File(originalPlugin.dataFolder, "speedbuilders/$structureFile")) } private fun selectStructure(difficulty: StructureDifficulty?): StructureData { @@ -478,7 +481,7 @@ class SpeedBuildersMinigame( 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 @@ -500,9 +503,14 @@ 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)) for ((playerUUID, playerArea) in playerAreas) { 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( 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 index 727ec49..56aa8c1 100644 --- 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 @@ -481,7 +481,7 @@ class HealthShopUI( } // process oak planks if (purchasedItems.any { it.key == "oak_planks" }) { - player.inventory.addItem(ItemStack.of(Material.OAK_PLANKS, 24)) + player.inventory.addItem(ItemStack.of(Material.OAK_PLANKS, 64)) } } } diff --git a/pgame-plugin/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 index 0402e47..f1477f1 100644 --- a/pgame-plugin/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.2 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/pgame-plugin/src/main/resources/config.yml b/pgame-plugin/src/main/resources/config.yml index b0856c2..2157163 100644 --- a/pgame-plugin/src/main/resources/config.yml +++ b/pgame-plugin/src/main/resources/config.yml @@ -1,39 +1,56 @@ -locations: - minigames: - 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= +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 + class: info.mester.network.partygames.game.HealthShopMinigame + display-name: Health Shop + 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 save-interval: 10 spawn-location: ==: org.bukkit.Location @@ -42,4 +59,7 @@ spawn-location: y: 93.0 z: 40.5 yaw: 90.0 - pitch: 0.0 \ No newline at end of file + pitch: 0.0 +mineguessr: + world: world + max-size: 1000 \ No newline at end of file diff --git a/pgame-plugin/src/main/resources/paper-plugin.yml b/pgame-plugin/src/main/resources/paper-plugin.yml index c29cf60..45eebee 100644 --- a/pgame-plugin/src/main/resources/paper-plugin.yml +++ b/pgame-plugin/src/main/resources/paper-plugin.yml @@ -10,7 +10,7 @@ dependencies: load: BEFORE required: true join-classpath: true - WorldEdit: + FastAsyncWorldEdit: load: BEFORE required: true join-classpath: true diff --git a/pgame-plugin/src/main/resources/speed-builders.yml b/pgame-plugin/src/main/resources/speed-builders.yml index e2bb29b..394cab9 100644 --- a/pgame-plugin/src/main/resources/speed-builders.yml +++ b/pgame-plugin/src/main/resources/speed-builders.yml @@ -2,40 +2,58 @@ 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 (?) mushroom: difficulty: EASY + display_name: Mushroom well: difficulty: EASY + display_name: Well, well, well # Medium difficulty bookshelves: difficulty: MEDIUM + display_name: Bookshelves & Enchanting Table dragon: difficulty: MEDIUM + display_name: Dragon enchanting: difficulty: MEDIUM + display_name: Enchanting Room bigportal: difficulty: MEDIUM + display_name: Big Portal warrior: difficulty: MEDIUM + display_name: Warrior # Hard difficulty 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 # Insane difficulty outpost: difficulty: INSANE + display_name: Outpost end_city: difficulty: INSANE + display_name: End City diff --git a/pgame-plugin/src/speed-builders-schema.json b/pgame-plugin/src/speed-builders-schema.json index 91c22d6..d56ceca 100644 --- a/pgame-plugin/src/speed-builders-schema.json +++ b/pgame-plugin/src/speed-builders-schema.json @@ -17,10 +17,15 @@ "HARD", "INSANE" ] + }, + "display_name": { + "type": "string", + "description": "Display name of the structure." } }, "required": [ - "difficulty" + "difficulty", + "display_name" ], "additionalProperties": false } 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 index 7ec39cf..3af081f 100644 --- a/test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/JavaMinigame.java @@ -57,6 +57,7 @@ public void start() { 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 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 index ecab49e..084ccf4 100644 --- a/test-minigame/src/main/kotlin/info/mester/network/testminigame/MinigamePlugin.kt +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/MinigamePlugin.kt @@ -1,13 +1,7 @@ package info.mester.network.testminigame -import com.mojang.brigadier.Command -import com.mojang.brigadier.arguments.StringArgumentType import info.mester.network.partygames.api.MinigameWorld import info.mester.network.partygames.api.PartyGamesCore -import io.papermc.paper.command.brigadier.Commands -import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents -import org.bukkit.Bukkit -import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin import org.bukkit.util.Vector @@ -41,32 +35,5 @@ class MinigamePlugin : JavaPlugin() { ), "Java Minigame", ) - // register /start command - @Suppress("UnstableApiUsage") - lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS) { event -> - val commands = event.registrar() - commands.register( - Commands - .literal("start") - .then( - Commands - .argument("bundle", StringArgumentType.word()) - .suggests { _, builder -> - val bundles = core.gameRegistry.getBundles() - bundles.map { it.name }.forEach(builder::suggest) - builder.buildFuture() - }.executes { ctx -> - val sender = ctx.source.sender - if (sender !is Player) { - return@executes 1 - } - val bundleName = StringArgumentType.getString(ctx, "bundle") - val players = Bukkit.getOnlinePlayers().toList() - core.gameRegistry.startGame(players, bundleName) - Command.SINGLE_SUCCESS - }, - ).build(), - ) - } } } 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 index 83f96c3..6ba504a 100644 --- a/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt @@ -11,12 +11,15 @@ import org.bukkit.inventory.ItemStack class PlaceBlockMinigame( game: Game, ) : Minigame(game, "place_block") { - override fun start() { - super.start() + override fun onLoad() { // access the world using game.world val world = game.world world.worldBorder.size = 30.0 world.worldBorder.center = startPos + } + + override fun start() { + super.start() audience.sendMessage(Component.text("Place blocks to win!", NamedTextColor.AQUA)) // get every player in the game with onlinePlayers From 4c62f3021e6a618e9d4758f2e89d7268a3f1d52b Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Sun, 26 Jan 2025 22:48:07 +0100 Subject: [PATCH 11/18] API: + Add Dokka for docs generation + Minigame: add handleCreatureSpawn to handle players using a spawn egg + Minigame: change parameter of startCountdown from milliseconds to ticks Plugin: + Damage Dealer: bug fixes + Speed Builders: add entity support + Statistics: track multiple statistics with placeholders + Sidebar: add statistics, remove beta text + Boosters: add basic booster system + Mineguessr: make chunk loading async --- build.gradle.kts | 1 + gradle.properties | 2 + pgame-api/build.gradle.kts | 4 +- .../mester/network/partygames/api/Game.kt | 10 +- .../mester/network/partygames/api/Minigame.kt | 46 +++-- .../partygames/api/PartyGamesListener.kt | 29 +++ pgame-api/src/main/resources/paper-plugin.yml | 2 +- pgame-plugin/build.gradle.kts | 2 +- .../network/partygames/BoosterManager.kt | 45 +++++ .../mester/network/partygames/Bootstrapper.kt | 10 +- .../network/partygames/DatabaseManager.kt | 161 ++++++++++++++++ .../mester/network/partygames/PartyGames.kt | 19 +- .../network/partygames/PartyListener.kt | 126 +++++++++++-- .../partygames/game/DamageDealerMinigame.kt | 31 ++-- .../partygames/game/GardeningMinigame.kt | 8 +- .../partygames/game/HealthShopMinigame.kt | 18 +- .../partygames/game/MineguessrMinigame.kt | 81 ++++---- .../mester/network/partygames/game/Queue.kt | 8 +- .../game/{GameManager.kt => QueueManager.kt} | 10 +- .../partygames/game/SnifferHuntMinigame.kt | 14 +- .../partygames/game/SpeedBuildersMinigame.kt | 175 +++++++++++++++--- .../LevelPlaceholder.kt | 6 +- .../{ => placeholder}/PlayingPlaceholder.kt | 10 +- .../placeholder/StatisticsPlaceholder.kt | 62 +++++++ .../sidebar/LobbySidebarComponent.kt | 35 ++++ .../partygames/sidebar/SidebarManager.kt | 6 +- .../network/partygames/util/LocationUtils.kt | 14 ++ .../network/partygames/util/WeightedItem.kt | 2 +- .../src/main/resources/speed-builders.yml | 21 ++- .../main/resources/speedbuilders/.gitignore | 3 + .../src/main/resources/speedbuilders/car.nbt | Bin 1722 -> 1725 bytes .../src/main/resources/speedbuilders/cart.nbt | Bin 0 -> 1599 bytes .../main/resources/speedbuilders/colors.nbt | Bin 0 -> 1253 bytes .../main/resources/speedbuilders/dragon.nbt | Bin 1481 -> 1475 bytes .../resources/speedbuilders/enchanting.nbt | Bin 1361 -> 1374 bytes .../resources/speedbuilders/first_house.nbt | Bin 0 -> 1319 bytes .../resources/speedbuilders/herobrine.nbt | Bin 0 -> 1277 bytes .../main/resources/speedbuilders/icecaps.nbt | Bin 0 -> 1235 bytes .../main/resources/speedbuilders/kitchen.nbt | Bin 0 -> 1613 bytes .../speedbuilders/speedbuilders_template.nbt | Bin 1927 -> 0 bytes .../speedbuilders/structure_template.nbt | Bin 1756 -> 0 bytes .../main/resources/speedbuilders/warrior.nbt | Bin 1508 -> 1490 bytes .../src/main/resources/speedbuilders/well.nbt | Bin 1589 -> 1589 bytes test-minigame/build.gradle.kts | 2 +- .../network/testminigame/JavaMinigame.java | 2 +- .../testminigame/PlaceBlockMinigame.kt | 2 +- .../network/testminigame/SimpleMinigame.kt | 3 +- 47 files changed, 793 insertions(+), 177 deletions(-) create mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/BoosterManager.kt rename pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/{GameManager.kt => QueueManager.kt} (95%) rename pgame-plugin/src/main/kotlin/info/mester/network/partygames/{level => placeholder}/LevelPlaceholder.kt (79%) rename pgame-plugin/src/main/kotlin/info/mester/network/partygames/{ => placeholder}/PlayingPlaceholder.kt (82%) create mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/StatisticsPlaceholder.kt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/.gitignore create mode 100644 pgame-plugin/src/main/resources/speedbuilders/cart.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/colors.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/first_house.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/herobrine.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/icecaps.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/kitchen.nbt delete mode 100644 pgame-plugin/src/main/resources/speedbuilders/speedbuilders_template.nbt delete mode 100644 pgame-plugin/src/main/resources/speedbuilders/structure_template.nbt diff --git a/build.gradle.kts b/build.gradle.kts index 779697f..54d74f9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ plugins { 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" } 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 index b443429..365ee26 100644 --- a/pgame-api/build.gradle.kts +++ b/pgame-api/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.github.johnrengelman.shadow") version "8.1.1" - id("io.papermc.paperweight.userdev") version "2.0.0-beta.13" + id("io.papermc.paperweight.userdev") + id("org.jetbrains.dokka") version "2.0.0" java } @@ -39,6 +40,7 @@ tasks { build { dependsOn("shadowJar") + dependsOn("dokkaGenerateModuleHtml") } test { diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt index c61b757..62a7cc8 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt @@ -220,7 +220,6 @@ class Game( // add all players to the game players.forEach { addPlayer(it) } // set up the game - audience.sendMessage(Component.text("Starting the game...", NamedTextColor.GREEN)) readyMinigames = bundle.minigames .shuffled() @@ -395,15 +394,14 @@ class Game( * 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) @@ -418,8 +416,8 @@ class Game( } else { "" } - append( - "${color}${i + 1}. ${topPlayer?.player?.name ?: "Nobody"} - ${topPlayer?.data?.score ?: 0}\n", + appendLine( + "${color}${i + 1}. ${topPlayer?.player?.name ?: "Nobody"} - ${topPlayer?.data?.score ?: 0}", ) } diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index df5a1a3..8071ec1 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -13,6 +13,7 @@ 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 @@ -38,7 +39,7 @@ import kotlin.random.Random abstract class Minigame( protected val game: Game, - name: String, + val minigameName: String, ) { private var _running = false private var countdownUUID = UUID.randomUUID() @@ -58,7 +59,7 @@ abstract class Minigame( init { val core = PartyGamesCore.getInstance() - val minigameConfig = core.gameRegistry.getMinigame(name)!! + val minigameConfig = core.gameRegistry.getMinigame(minigameName)!! originalPlugin = minigameConfig.plugin worldIndex = Random.nextInt(0, minigameConfig.worlds.size) rootWorldName = minigameConfig.worlds[worldIndex].name @@ -79,12 +80,14 @@ 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() {} /** * 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() {} @@ -118,29 +121,30 @@ 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: Runnable, ) { @@ -150,7 +154,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 { @@ -173,7 +177,7 @@ abstract class Minigame( } fun startCountdown( - duration: Long, + duration: Int, onEnd: Runnable, ) { startCountdown(duration, true, onEnd) @@ -200,6 +204,12 @@ abstract class Minigame( open fun handleEntityShootBow(event: EntityShootBowEvent) {} + open fun handleCreatureSpawn( + event: CreatureSpawnEvent, + player: Player, + ) { + } + // block events open fun handleBlockPhysics(event: BlockPhysicsEvent) {} @@ -233,6 +243,12 @@ abstract class Minigame( open fun handleInventoryOpen(event: InventoryOpenEvent) {} + open fun handleInventoryClick( + event: InventoryClickEvent, + clickedInventory: Inventory, + ) { + } + // game events open fun handleDisconnect( player: Player, @@ -242,12 +258,6 @@ abstract class Minigame( open fun handleRejoin(player: Player) {} - open fun handleInventoryClick( - event: InventoryClickEvent, - clickedInventory: Inventory, - ) { - } - abstract val name: Component abstract val description: Component } diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt index 4c39c56..ec7e7d4 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt @@ -14,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,11 +46,14 @@ import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.event.player.PlayerSwapHandItemsEvent import org.bukkit.event.player.PlayerToggleFlightEvent import org.bukkit.inventory.EquipmentSlot +import org.bukkit.inventory.meta.SpawnEggMeta +import java.util.UUID class PartyGamesListener( private val core: PartyGamesCore, ) : Listener { private val gameRegistry = core.gameRegistry + private val spawnEggUseMap: MutableMap = mutableMapOf() private fun getMinigameFromWorld(world: World) = gameRegistry.getGameByWorld(world)?.runningMinigame @@ -274,6 +279,16 @@ class PartyGamesListener( return } 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 @@ -336,4 +351,18 @@ class PartyGamesListener( 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/pgame-api/src/main/resources/paper-plugin.yml b/pgame-api/src/main/resources/paper-plugin.yml index 0b7e2e0..bf7ad90 100644 --- a/pgame-api/src/main/resources/paper-plugin.yml +++ b/pgame-api/src/main/resources/paper-plugin.yml @@ -3,7 +3,7 @@ version: '1.0' main: info.mester.network.partygames.api.PartyGamesCore api-version: '1.21' bootstrapper: info.mester.network.partygames.api.Bootstrapper -load: POSTWORLD +load: STARTUP authors: - Mester description: Core API for Party Games diff --git a/pgame-plugin/build.gradle.kts b/pgame-plugin/build.gradle.kts index 755da80..f695027 100644 --- a/pgame-plugin/build.gradle.kts +++ b/pgame-plugin/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("com.github.johnrengelman.shadow") version "8.1.1" - id("io.papermc.paperweight.userdev") version "2.0.0-beta.13" + id("io.papermc.paperweight.userdev") java } 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..95f225e --- /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.5) + } + if (player.hasPermission("group.pro")) { + return Booster("Pro Booster", 1.2) + } + if (player.hasPermission("group.advanced")) { + return Booster("Advanced Booster", 1.1) + } + 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 index 2a1b3fc..fd87a54 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt @@ -3,9 +3,9 @@ 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.GameType import info.mester.network.partygames.game.HealthShopMinigame import info.mester.network.partygames.game.MineguessrMinigame +import info.mester.network.partygames.game.QueueType import info.mester.network.partygames.game.SnifferHuntMinigame import info.mester.network.partygames.game.SpeedBuildersMinigame import io.papermc.paper.command.brigadier.Commands @@ -89,11 +89,11 @@ class Bootstrapper : PluginBootstrap { return@executes 1 } val typeRaw = StringArgumentType.getString(ctx, "game").uppercase() - if (!GameType.entries.any { it.name.uppercase() == typeRaw }) { + if (!QueueType.entries.any { it.name.uppercase() == typeRaw }) { return@executes 1 } - val type = GameType.valueOf(typeRaw) - val currentQueue = PartyGames.plugin.gameManager.getQueueOf(sender) + val type = QueueType.valueOf(typeRaw) + val currentQueue = PartyGames.plugin.queueManager.getQueueOf(sender) if (currentQueue != null && currentQueue.type == type) { sender.sendMessage( Component.text( @@ -103,7 +103,7 @@ class Bootstrapper : PluginBootstrap { ) return@executes 1 } - PartyGames.plugin.gameManager.joinQueue(type, listOf(sender)) + PartyGames.plugin.queueManager.joinQueue(type, listOf(sender)) Command.SINGLE_SUCCESS }, ).build(), 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 index de5fda1..08ff1d3 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt @@ -23,6 +23,36 @@ class DatabaseManager( // 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(), + ) } } @@ -49,4 +79,135 @@ class DatabaseManager( 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 + } } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt index cb46ebf..2203053 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt @@ -2,9 +2,11 @@ package info.mester.network.partygames import info.mester.network.partygames.api.MinigameWorld import info.mester.network.partygames.api.PartyGamesCore -import info.mester.network.partygames.game.GameManager +import info.mester.network.partygames.game.QueueManager 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 @@ -12,7 +14,6 @@ import net.megavex.scoreboardlibrary.api.exception.NoPacketAdapterAvailableExcep import net.megavex.scoreboardlibrary.api.noop.NoopScoreboardLibrary import org.bukkit.Bukkit import org.bukkit.GameRule -import org.bukkit.Location import org.bukkit.World import org.bukkit.entity.Player import org.bukkit.plugin.java.JavaPlugin @@ -65,13 +66,14 @@ fun Int.pow(power: Int): Double = this.toDouble().pow(power) class PartyGames : JavaPlugin() { lateinit var core: PartyGamesCore - lateinit var gameManager: GameManager + 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 { private var _plugin: PartyGames? = null @@ -100,7 +102,6 @@ class PartyGames : JavaPlugin() { fun reload() { reloadConfig() - spawnLocation = config.getLocation("spawn-location")!! registerMinigames() } @@ -122,12 +123,14 @@ class PartyGames : JavaPlugin() { // 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.register() LevelPlaceholder(levelManager).register() + StatisticsPlaceholder(databaseManager).register() // set up event listeners server.pluginManager.registerEvents(PartyListener(this), this) // init all worlds @@ -167,7 +170,7 @@ class PartyGames : JavaPlugin() { // register bundles for each minigame, then family night core.gameRegistry.registerBundle( this, - listOf("healthshop", "speedbuilders", "gardening", "damagedealer"), + listOf("healthshop", "speedbuilders", "gardening", "damagedealer", "mineguessr"), "familynight", "Family Night", ) 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 index ec65a62..30b1019 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt @@ -7,22 +7,32 @@ 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.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.NamespacedKey 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.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 class PartyListener( private val plugin: PartyGames, ) : Listener { - private val gameManager = plugin.gameManager + private val gameManager = plugin.queueManager private val core = plugin.core private val sidebarManager = plugin.sidebarManager @@ -70,7 +80,7 @@ class PartyListener( @EventHandler fun onPlayerQuit(event: PlayerQuitEvent) { - gameManager.getQueueOf(event.player)?.removePlayer(event.player) + gameManager.removePlayerFromQueue(event.player) plugin.sidebarManager.unregisterPlayer(event.player) } @@ -91,7 +101,7 @@ class PartyListener( if (plugin.core.isAdmin(event.player)) { return } - plugin.gameManager.getQueueOf(event.player)?.handlePlayerInteract(event) + plugin.queueManager.getQueueOf(event.player)?.handlePlayerInteract(event) } @EventHandler @@ -129,24 +139,30 @@ class PartyListener( @EventHandler fun onGameEnded(event: GameEndedEvent) { val timeElapsed = (Bukkit.getCurrentTick() - event.game.startTime) * 0.05 - // increase everyone's xp based on the score + // increase the xp of players for ((player, data) in event.topList) { - val oldLevel = plugin.levelManager.levelDataOf(player.uniqueId) // XP is points gained + 15 per half a minute val xpFromPlayTime = (timeElapsed.toInt() / 30) * 15 - val addedXp = data.score.coerceAtLeast(0) + xpFromPlayTime - plugin.levelManager.addXp(player.uniqueId, addedXp) - val newLevel = plugin.levelManager.levelDataOf(player.uniqueId) + val xpFromScore = data.score.coerceAtLeast(0) + plugin.databaseManager.addPointsGained(player.uniqueId, event.game.bundle.name, xpFromScore) + plugin.databaseManager.addTimePlayed(player.uniqueId, event.game.bundle.name, timeElapsed.toInt()) + // process boosters + val boosters = plugin.boosterManager.getBooster(player) + val boosterMultiplier = boosters.fold(1.0) { acc, booster -> acc * booster.multiplier } + val finalXp = ((xpFromScore + xpFromPlayTime) * boosterMultiplier).toInt() + val oldLevel = plugin.levelManager.levelDataOf(player.uniqueId) + plugin.levelManager.addXp(player.uniqueId, finalXp) val onlinePlayer = Bukkit.getPlayer(player.uniqueId) ?: return + 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") + appendLine(" -> ${newLevel.level} LEVEL UP!") } else { - append("\n") + appendLine() } append("Progress: ") append("${newLevel.xp} [") @@ -172,13 +188,26 @@ class PartyListener( for (i in 0 until maxSquares - filledSquares) { append("■") } - append("] ${newLevel.xpToNextLevel}\n") - append("+${data.score} XP (Points Gained)\n") - append("+$xpFromPlayTime XP (Time Played)\n") + appendLine("] ${newLevel.xpToNextLevel}") + appendLine("+${data.score} XP (Points Gained)") + 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 @@ -202,4 +231,75 @@ class PartyListener( } 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.persistentDataContainer.set( + NamespacedKey(plugin, "spawned"), + 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( + NamespacedKey(plugin, "spawned"), + PersistentDataType.BOOLEAN, + ) + ) { + target.remove() + } + } } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt index 3519030..a508892 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt @@ -16,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 @@ -186,8 +187,7 @@ class DamageDealerMinigame( 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) } @@ -198,7 +198,7 @@ class DamageDealerMinigame( "- You only have 20 tries!" audience.sendMessage(mm.deserialize(introductionMessage)) - startCountdown(3 * 60 * 1000) { + startCountdown(3 * 60 * 20) { end() } } @@ -209,15 +209,16 @@ class DamageDealerMinigame( } 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() @@ -232,8 +233,8 @@ class DamageDealerMinigame( } 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) } } @@ -241,12 +242,10 @@ class DamageDealerMinigame( 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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt index c79353d..f0a2279 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt @@ -356,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 @@ -574,8 +574,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/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index 9f31f11..85ea818 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -281,7 +281,7 @@ class HealthShopMinigame( ) } // start a 30-second countdown for the shop state - startCountdown(30000) { + startCountdown(30 * 20) { startFight() } } @@ -315,7 +315,7 @@ class HealthShopMinigame( } } // start a 3-minute countdown for the fight - startCountdown(3 * 60 * 1000) { + startCountdown(3 * 60 * 20) { end() } // start the supply chest timer @@ -782,12 +782,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 = Component.text("Health Shop", NamedTextColor.AQUA) + 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 index d3537c0..e70497b 100644 --- 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 @@ -16,6 +16,7 @@ import org.bukkit.World import org.bukkit.block.Biome import org.bukkit.event.block.BlockPhysicsEvent import java.util.UUID +import java.util.concurrent.CompletableFuture import kotlin.random.Random private fun levenshteinDistance( @@ -54,7 +55,7 @@ class MineguessrMinigame( companion object { private var sourceWorld: World = Bukkit.getWorld("world")!! private var maxSize = 1000 - private val disallowedBiomes = emptyList() + private val disallowedBiomes = listOf(Biome.DRIPSTONE_CAVES) init { reload() @@ -76,14 +77,17 @@ class MineguessrMinigame( private val guessed = mutableListOf() private var selectedBiomeIndex = 0 - private fun getRandomChunk(): ChunkSnapshot { + 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) - return sourceWorld.getChunkAt(chunkX, chunkZ, true).getChunkSnapshot(true, true, false, false) + val future = sourceWorld.getChunkAtAsync(chunkX, chunkZ, true) + return future.thenApply { chunk -> + chunk.getChunkSnapshot(true, true, false, false) + } } private fun copyChunk(chunk: ChunkSnapshot) { @@ -119,42 +123,47 @@ class MineguessrMinigame( game.world.refreshChunk(0, 0) } - private fun loadChunk() { + private fun loadChunk(): CompletableFuture { audience.sendActionBar(Component.text("Loading chunk...", NamedTextColor.YELLOW)) biomeList.clear() - val chunk = getRandomChunk() - copyChunk(chunk) - audience.sendMessage( - MiniMessage.miniMessage().deserialize( - "${"-".repeat(30)}\n" + - "Loading finished, time to guess!\n" + - "This chunk contains ${biomeList.size} biomes.", - ), - ) - selectedBiomeIndex = Random.nextInt(0, biomeList.size) - // turn biome text into underscores, "EXAMPLE_BIOME" -> "_______ _____" - val biomeText = - biomeList[selectedBiomeIndex] - .name() - .map { - if (it == '_') { - ' ' - } else { - "_" - } - }.joinToString("") - audience.sendMessage( - MiniMessage.miniMessage().deserialize("Hint for one biome: $biomeText"), - ) + 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.", + ), + ) + selectedBiomeIndex = Random.nextInt(0, biomeList.size) + // turn biome text into underscores, "EXAMPLE_BIOME" -> "_______ _____" + val biomeText = + biomeList[selectedBiomeIndex] + .name() + .map { + if (it == '_') { + ' ' + } else { + "_" + } + }.joinToString("") + audience.sendMessage( + MiniMessage.miniMessage().deserialize("Hint for one biome: $biomeText"), + ) + + CompletableFuture.completedFuture(null) + } } private fun startRound() { state = MineguessrState.LOADING - loadChunk() - guessed.clear() - state = MineguessrState.GUESSING - startCountdown(15 * 1000) { - finishRound() + loadChunk().thenRun { + guessed.clear() + state = MineguessrState.GUESSING + startCountdown(15 * 20) { + finishRound() + } } } @@ -193,11 +202,13 @@ class MineguessrMinigame( guessed.clear() } + override fun onLoad() { + game.world.setGameRule(GameRule.REDUCED_DEBUG_INFO, true) + } + override fun start() { super.start() - game.world.setGameRule(GameRule.REDUCED_DEBUG_INFO, true) - for (player in game.onlinePlayers) { player.gameMode = GameMode.SPECTATOR } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Queue.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Queue.kt index bee84e1..340245c 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Queue.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Queue.kt @@ -93,9 +93,9 @@ private class CountdownTask( } class Queue( - val type: GameType, + val type: QueueType, val maxPlayers: Int, - private val manager: GameManager, + private val manager: QueueManager, ) { private val players = mutableListOf() private val countdownTask = CountdownTask(this) @@ -232,7 +232,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 +256,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/GameManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/QueueManager.kt similarity index 95% rename from pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt rename to pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/QueueManager.kt index e23ad06..9b34179 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GameManager.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/QueueManager.kt @@ -6,7 +6,7 @@ import net.kyori.adventure.text.minimessage.MiniMessage import org.bukkit.entity.Player import java.util.UUID -enum class GameType( +enum class QueueType( val displayName: String, ) { HEALTHSHOP("Health Shop"), @@ -19,7 +19,7 @@ enum class GameType( private val mm = MiniMessage.miniMessage() -class GameManager( +class QueueManager( plugin: PartyGames, ) { private val core = plugin.core @@ -27,7 +27,7 @@ class GameManager( private val queues = mutableMapOf() private fun createQueue( - type: GameType, + type: QueueType, maxPlayers: Int = 8, ): Queue { val queue = Queue(type, maxPlayers, this) @@ -36,7 +36,7 @@ class GameManager( } private fun getQueueForPlayers( - type: GameType, + type: QueueType, players: List, ): Queue { // either return the first queue that can still fit the players, or create a new queue @@ -52,7 +52,7 @@ class GameManager( } fun joinQueue( - type: GameType, + type: QueueType, players: List, ) { // check if there is a player that is already in a game diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt index 0ac7f3a..2849d00 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SnifferHuntMinigame.kt @@ -160,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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt index 043be7d..489fe74 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt @@ -9,7 +9,9 @@ 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.format.NamedTextColor import net.kyori.adventure.title.Title @@ -18,6 +20,7 @@ import org.bukkit.Bukkit 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 @@ -30,15 +33,20 @@ 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 @@ -77,7 +85,7 @@ private fun Location.toBlockVector(): BlockVector3 = BlockVector3.at(blockX, blo 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()) } @@ -148,7 +156,7 @@ class SpeedBuildersMinigame( // 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 } @@ -229,7 +237,32 @@ class SpeedBuildersMinigame( } correctBlocks++ } - return correctBlocks.toDouble() / originalBlocks.size + 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 + correctEntities).toDouble() / (originalBlocks.size + originalEntities.size) } private fun calculateAccuracy( @@ -247,6 +280,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, @@ -255,6 +313,10 @@ class SpeedBuildersMinigame( blocks.forEach { block -> giveItemFromBlock(block, player) } + val entities = structure.entities + entities.forEach { entity -> + giveItemFromEntity(entity, player) + } } private fun clearPlayerArea( @@ -274,6 +336,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( + NamespacedKey(originalPlugin, "spawned"), + PersistentDataType.BOOLEAN, + ) + }) { + entity.remove() + } } private fun eliminatePlayer(playerUUID: UUID) { @@ -400,8 +471,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 @@ -462,9 +532,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 { @@ -478,6 +561,41 @@ class SpeedBuildersMinigame( } } + override fun handleCreatureSpawn( + event: CreatureSpawnEvent, + player: Player, + ) { + val snappedAngle = snapTo90(player.location.yaw) + event.entity.setRotation(snappedAngle, 0.0f) + event.entity.setAI(false) + event.entity.persistentDataContainer.set( + NamespacedKey(originalPlugin, "spawned"), + 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( + NamespacedKey(originalPlugin, "spawned"), + 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 @@ -510,6 +628,7 @@ class SpeedBuildersMinigame( audience.sendTitlePart(TitlePart.TITLE, mm.deserialize("${currentStructureData!!.displayName}")) audience.sendMessage(Component.text("Memorise the structure!", NamedTextColor.GREEN)) for ((playerUUID, playerArea) in playerAreas) { + clearPlayerArea(playerArea, false) val player = Bukkit.getPlayer(playerUUID) ?: continue // place down the structure in the play area val location = playerArea.toLocation(startPos.world) @@ -526,7 +645,7 @@ class SpeedBuildersMinigame( player.teleport(location.clone().add(-0.5, 1.0, -0.5)) player.isFlying = true } - startCountdown(10000) { + startCountdown(10 * 20) { startBuild() } } @@ -541,8 +660,21 @@ class SpeedBuildersMinigame( player.inventory.clear() giveItemsFromStructure(currentStructure!!, player) } + // remove every entity spawned by the structure + for (entity in game.world.entities.filter { + it.persistentDataContainer.has( + NamespacedKey( + originalPlugin, + "spawned", + ), + PersistentDataType.BOOLEAN, + ) + }) { + entity.remove() + } audience.sendMessage(Component.text("Build the structure!", NamedTextColor.GREEN)) - startCountdown(30000) { + + startCountdown(30 * 20) { startJudge() } } @@ -581,7 +713,7 @@ class SpeedBuildersMinigame( } } // 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) } @@ -603,11 +735,7 @@ class SpeedBuildersMinigame( 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() } } @@ -617,6 +745,9 @@ class SpeedBuildersMinigame( private fun win() { 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() } @@ -637,13 +768,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 30 seconds to replicate the structure.", + NamedTextColor.AQUA, + ) } diff --git a/pgame-plugin/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 pgame-plugin/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/pgame-plugin/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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/PlayingPlaceholder.kt similarity index 82% rename from pgame-plugin/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..80ced46 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PlayingPlaceholder.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/PlayingPlaceholder.kt @@ -1,6 +1,6 @@ -package info.mester.network.partygames +package info.mester.network.partygames.placeholder -import info.mester.network.partygames.game.GameType +import info.mester.network.partygames.game.QueueType import me.clip.placeholderapi.expansion.PlaceholderExpansion import org.bukkit.entity.Player import java.util.concurrent.ConcurrentHashMap @@ -9,15 +9,15 @@ class PlayingPlaceholder : PlaceholderExpansion() { private val playingMap: MutableMap = ConcurrentHashMap() init { - val gameTypes = GameType.entries.map { it.name } - for (gameType in gameTypes) { + val queueTypes = QueueType.entries.map { it.name } + for (gameType in queueTypes) { addPlaying(gameType, 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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/LobbySidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/LobbySidebarComponent.kt index 1c05fdb..389a4b7 100644 --- a/pgame-plugin/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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt index 84e2932..acafecd 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/SidebarManager.kt @@ -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,7 +112,7 @@ class SidebarManager( } fun openQueueSidebar(player: Player) { - val queue = plugin.gameManager.getQueueOf(player) ?: return + val queue = plugin.queueManager.getQueueOf(player) ?: return createSidebar(player, createQueueLayout(queue)) } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/LocationUtils.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/LocationUtils.kt index c555951..65aab7d 100644 --- a/pgame-plugin/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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/WeightedItem.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/WeightedItem.kt index 6ed86d6..dc76411 100644 --- a/pgame-plugin/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/speed-builders.yml b/pgame-plugin/src/main/resources/speed-builders.yml index 394cab9..f8490a6 100644 --- a/pgame-plugin/src/main/resources/speed-builders.yml +++ b/pgame-plugin/src/main/resources/speed-builders.yml @@ -12,12 +12,18 @@ structures: japanese_idk: difficulty: EASY display_name: Japanese Arch Thingy (?) - mushroom: - difficulty: EASY - display_name: Mushroom 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 # Medium difficulty bookshelves: difficulty: MEDIUM @@ -34,6 +40,12 @@ structures: warrior: difficulty: MEDIUM display_name: Warrior + cart: + difficulty: MEDIUM + display_name: Cart + colors: + difficulty: MEDIUM + display_name: Colors # Hard difficulty tree: difficulty: HARD @@ -50,6 +62,9 @@ structures: graveyard: difficulty: HARD display_name: Graveyard + icecaps: + difficulty: HARD + display_name: Ice Caps # Insane difficulty outpost: difficulty: INSANE 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/car.nbt b/pgame-plugin/src/main/resources/speedbuilders/car.nbt index 49970d90d133bb1ff27d44ef135c5d1b3001eb08..b1aa347312e963f292991130f47e90e37346e709 100644 GIT binary patch literal 1725 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d343512584047957caaa53ff5b0c6fa175af78cf GIT binary patch literal 1253 zcmZWmYfMs69Cxi{&73XUa$349ZR$*y2!<{{WUgFjn_F%zWacZD!YabY70S#QGgj7$ z!pK%C>+NF`oWqu-vZmfCH6R{NG2cR_!VrVbxpWcgez@n}|Ks;MVRc)5u*WITpp1#4 zp!6m5+R^(>uf9?xbq2+oVtrxRH${zZmw)wE^{MyIgw)Yl{;_y-Mg8ff8O4vmtXCQE}F9Hm>zoM@(u}aF)@m9G7X#8MfIw_QO>glZtjHpqb!gHHT&y>1642 zJ`1()=RIN?2+W3}F8Ua8@v<^PL6=v@DfcggBi70m5`_xNff{-^(%7m@V~hL{sSX^9%qw}!hYkD6 zxpVh^rzbxq}4GbHFx|=+v(ab6@b^RRn8g>A7X1 znX8hQ{K$6FHY8h@nIBSCNK?1O*aiJW(-()yMi(xSM1J;OE>*LeJpj~-Biof}78`P% zgnhF3#ufflZ%=Dij-@tM)-L=3`;b@Z3-kc?d0bm&Aaxn=XThVMbHbGaOrY~pjwNKM zmLoIVu*iXt10v!Gi{Fw(NE}JCMCM^3VYDtg+MD1<+PC;u3oP!|Ov_hrVvnF8KNbbR zNc<0o2Z3B-u`xmExr1zwN+y&fR?|z2;~0`QK>`A<7au+g6W)Ll!_DTO(h_Egv9b+-ubu}R^L>T` zP(cygP8O6t(E!JA(p_uhdv@Vg6+_M0i`Wdoy@kBPJlg3(czM z($1hpHd)-l6OQR0IHXJT=&+~6ZaOE(nZj9b->agfzPEO-=8?ndCRAN&MyQj_;ilXk zd)Lu8UGlqBNA>+M_|Xw>3YGi}(hg*CSPR(E)!qL!&eT0lIvCc25% zw2e}+N?aF@w$jcM1UyoqE`lth&P)N#u(D}@YKtg%?0qrTgK{I2gqvt1qR z&sIl6OrGw_v52QzehSy&&jY-@i(KhH;VC1PzLgI~avl<>&wQ`qcx5$FU#&``7S%N5 zl}C@VqqT1elqId)dW_t7*3_50AeWExx>Wl0b$plCU|hI4LSvq$=dCDsj5t1*zVXsu zes>Ue#@g(&fGG4i8PY8&M}^(L^j@S-O6O$cdBvP#i>v%9A=3LgQYU0zYNSMCaM*i? z=a|-DQl};73$1a|)6Xp+yG)p`Y^^_IA#s0Z)*iX%tVX0UuSuMTnKMI2? zXMJiUwIS!W3n1X7dp53qvcD!72!5?|uzy;`5YcVFHSmw` zQ)@YUz*)hrGdl_U010QG3jxE(F!9(No()-Nh<~pB(;yqr=2*=AkBt?{*48xDgVNKf z^foRFwIFp%lAqh^n3`D0cHSbyXf^NWi-yhN%HnCy8rm4wkh1oNFGb|BIuVr8W5CR8mMnZy zR~*}+mEZB=dG*vjrlY1b>WiVgx+j#WiV9Ymw0Pp}(|Oj-B@wHOXsl~B9jW2jQiX)v z+=L=l=+Lt;@wZk@!MRP&mtnUb{$*2iQW~3OjAB+-?%2(qnG5EIzUavH8RNfRU8OUV z-z}?xRvEh420kmxGWMdMRrt*tK2g)FPu544T&$*L@fKB$zvK*d6-dy%nQveH*Hh}? Mv7vQ8L2z*RANz6t%>V!Z literal 1481 zcmb2|=3oGW|7&L-%)9L-!B#)X+bb-9h4m5x>#g<|YhUbqzG!(ItJk93Ln13DiS#_- z{I#dH$t%zP;-;1L-vXH11hcoCzNuQ?qY^jsZluJk^`U8L&(9xySzP&V*3I_#(;NRR ze$~D!`>fr++VrE#UT*V0d`jo1_U0Oi(;KZeE;rodY@X5avU#RTlc(~GH>Q)%7%MiN zRDPxRJ!3;*rDvb+9Ibel*|J;Wd~DD2d2wk;$BID;KHgdW`|-E0 z9iB=O=WTbNInwi7Z&|@HPQkdOJ+`8cD_c+doT-4Y-)7o9uWSAYGUjKhTAEF{;XI&W zdpp4Pd_4EzkIL)dLV=Eezto5Lel5KtfLjzb)LPNb@}J>FI^|^ZP%>a9dAAZ z>{{*b2IpgTKL6~~6aLV^v!y!ejh^P+vf)x-iuT}R(?hVvx2~zg-0nqg6&DBC^YC!1}>H;w1b<6^&k^av`KtAOcL<&iu z^~kn9KZYs?_CVUN{hC0(a)G?5eE=9Y>lnVP90J7=Bzi!M%N$^@!2|93W=Cl7!G%y5 z-)+zg=teWZzFr4pRURYALEIp#qZFH*_x_Ib@y!LLMA7@PxBEL`1ueUE3eZ5fAJpUQTYZ`1fA-zFdi?LNFLytFJZ-Sf{P^A8ZvTFE ziEr)U-uvy>AN_CiYWw9Cp}V8M-(9=#(7Nip<=<=THW$6lz4>m|tFyDym;KAH|I&VR z|9fejpP5pBc4vNwjkha#`)i}t{kR*O-us!q`zdZUdA|Q+nO)Ccm8{Qr{5~ggYjUky R{Ji+z?5gHr?Fv>53;-Zs{`UX? diff --git a/pgame-plugin/src/main/resources/speedbuilders/enchanting.nbt b/pgame-plugin/src/main/resources/speedbuilders/enchanting.nbt index a2670ba2af74cdcf3587cae1eb42f359b8ab91ba..34571153ccd65a3e1a0337bf6920e680208f7e46 100644 GIT binary patch literal 1374 zcmYjQe^8Ql7Q3ot7Ey4qp|Q8mG1-r-?t-++H;* z=CX@4Ai7N#RNSo}g8cC81x-;&?bT_BQ%t5#w4rfIu6b{DLph_z{G$XWK{O%ZcHNQC#bB*H50zUIsxE~Q{k=DQS#v^iU64eWSa~Sx z#o~vnr9T>;Lv&YbV(QhNY?%vfN{jwNw4QIZj?b}r%7@F5*?#3y#e>@Fl0_G7-Z?dm z7)!1maVAG+mlAxfReSFoh8lPwrxsG{sKTtRh7?)z_OgkUbyTw<($3a#XU4BSN{SgH z2j$}ZR2hgn9CQ}Zd*&S^caC3a4qa%D!RxmK<%Zfw6h9Tn04(|~-fvnhe54f%PA>(X zbLZF_oHU!IlUjx4MFRcnzRnh%2eh=XzXOcjkWU#_*(SF9ZD~wS-Cfq^9gnLEh@k=y z)qJsS0UPozRtki;?%0?Q0ym>KROs@mJW{zmwM}`O?N<&}H(* z_q?4pdOY5rOY8f<@66gXk+JYpakrReO=Jh#CK8vz`=o}?l~ zcy#uNfyq$Cqq&U$;r7jK9wV5^*Z*Sxk(@@&H}IWzFnb^$P^u<0T0X^2A(pb>-h^bB zZu}XOe5cv)_&f=;vOY?}bt)@)qNa_##F*C{BcXxWc9==i{4sLbN0C*q7LgMmLxqY! z!IQ>0de#M@A(D64N5LsbK;%TIbCv@$shXM*6c;+we?z=gb&~s&v`}Kcqqjqez! z0Mdq^IgV`p7&6Rmf|(S}xse2%f{nogAaqt1)qz&1h|jl^)>Doe{kc@Oz01@Q7lN-) zg|`}T=r+SOFfuL#Yvm>(K1koj*s-6&B&Ac1miZ}YrpK%xKxYSOoQ8L-g(s`oIZ+*r zJNM`9R+aaS6B~UcW`_DU+t68O#(pIK2U%Dbfq>0}yWB=U)yNIw`E4E8V!fq02gx;ldylU#dF*ELZ%ZCQQo5 z#K4UwytX7<(mr6yq;?DfUYy<=1u7ygfrQEJ*AoO(6h8pYf=EjQmnQ7eF-C?L_o$q( z$Hq4c;nLgdC9A-85*xfQ2R{OflWtyN4o85MVYzq)^`hHdXBJko&(6+l0mExfxrciU zU#^mm0AwKGyj;Zt|N2{N5tcgx_`ik78MVYtjXu?OAKz5#LJqadTj!>1XnN6~KC$J) zEuBbxDmO7(JmZTj749i^raa$$s})IQiTj$O=LI7UAsw>ZT?g59Sq%c?y_ejE;&u7u zs4^lK=||~#udeA7&Ms$>R%5$j}LPS>c1Z?A`5*!oL#nv$j=-9Hf*rs^I12o@B0 zOzn`AYF;j6ss}ZzN0BSmn@?{ym`v6*PApOw(KQg6F-;pf-#Q#SRebQ?T*TyA%dc%= HUS9tLlNZo{ literal 1361 zcmZ8ge^8Ql94@!qS}*e|4VY28Jk8dnyb_YBIcL*;Fz5V5?()}bnR?DjOh8k2eP86xZv6B8-skf?pZD{9o`;>h zx7Rs-D|G^0-t$EF$m-GRqqqdG-dKN|udLnVG zY#BfOklJaNUb`CxmCq@6jQNV5$*vw)A*%7zKt4j$q&4<0tXJcW$Sww4Q%oUn8!CNIfu2yl_X!tkHCm*|2Qa^oU%leZStnKBu|}1R zgPDM8RoCzxvyf$jKKb!i#&7K&9VC7(QIRI>-_Pc1gXN)~RG6PbRHfM%#GgyKT+4B4 zPb$*Bv}Nl|J#JV784+I^Nz7|Isrti@lTT(BQw%MJ2HzLCO>*#{iYRJ9Y2)oG-0+!8 zlCr^r@z{bEMND!`UPaq|UqE=-{-qQrE5`NnR#|PJjEiG&}oY=qscMq*1W+wg|HljDKUq;g2>33&$x~id$ zvbQ26$LxSvXE_c{eK12iW($!+Q#o%i;My(>Aaw_xGeJP+Rbf~40K(pJO2cbQI=U*V z)aGK|iK^9V@Fo>tK1B7+5u+abWOj9W0ZN&U(y9>)NgUJ&ATuX(yM)(5d%HR3uS*u$oyR zheqye9M?diOeR_0g-=n<{!+5<K()BQg8pMxItAp`E6097{Y8GI){V+3tX~ zErQG(PcYycf1FIfh%$fBzCa#4ifN7=QB@>aT|1-wjcpnhr&|ur`8U3*E}z*Cm#WC%W;%FCp9nn^u#^W zE78t~qW=lhEXYjF$3~RY0S{BX8SoR@P^c%!kM*(?cfSgNo)izsa7N_MqDp`$^8tm{ zA=!HXQGP{=v*mv5xD}H?vvoIU8iv3`R=x@Izbsg}x=+J4fnbsH)}!w2`#u>`i8qKD z94W5x{N$RmPf<)2NJFI;1KvNOL_eN2P_u@j$pP~K*x89fqvDLjDoPLbe~qF2x>$OP zXR=1tcjg-1D0@UZ)c&b7!CX2egiuPLxTSJb>AkE6!I1UU+=7m+c=g>5xAOC>&+R^% zQTZ6W#4H^xWJ>IJb%*@af8WztE@%mf6U$0W*)2<_eSV=twxXY(UktU&3rTvNDbdZ} zK5cG&Ec*TIJ%79Zge|*hu6&*+&!H?WPArZNDro{r_{Dz;LoiaaWs!A}I+K0EG$>KF blgdV#7vyTxZuEShwy!m&?Hk 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 0000000000000000000000000000000000000000..414550896c137b05a9a037cb2cb1c42c4061db80 GIT binary patch literal 1319 zcmb2|=3oGW|4V1z?Yk8w;?`e#&X6Z`^{SxwW!LwvtGc>dt0>*4$;sf*x<%hOf8A*H zv0uz`<8`-{kQ8s^k2j``drzFt399l+z4h1j$&^b^w^`l4HvPP1?dM|q@B4X|XPV0e z{rdNP`IWyL<>%k`pFL$(ebK7Rcg`Q4X&bLBf82BWjyi+O5>Tb<-~<|h98dg`BEh=y!YPgA|d06yJvvRyeA1Uqd-Id${Nb{hb0u%>EeSI_cj{Q=QH#|Q z=~JuIw{CwwG48%L%sX>tu0B}8cv<3|ZxYbC5PN}s0XixBy6d~#U61FzH=Y=teddmo zaeDHzn*3+%J|@LxY3GieIXYvfgt2(?yreV1y_KsB`)9nc)K~ju*WEeqGuZ3Z={veR zA)y2}dETUdF-Lgb{+zwfJ|W}${V#tRovZh;em_(aH=iba8K96(4NXJLx0m}E05VEtzi99E9R`L zP;PjIsYo1|@qJ58T3ghFgRwkY6`FTTWI3_{t;-kM-Mvt30+7qN6U3;VePgfbv&`Pg zO@{tv&rTk*xjAFG#Agw(=X!vi1KJ|!{CnoS)uo^yJr9nV4ExEuC4g+bwr|EemoM3W zI<4(nF(c6Z^SO6)FZ>&GU@tJ9FVv}lL{7%?yiEru5HNf1e4syp4!!QT1muXS^Zww7 zz9L(s-?m%g72i%^zNlsfNreObXS%Kls3Zmw zoW06m?c&b8%HOX6gBOWk{+1PL;_p&_Ly&?bpo5|2gBaDOca8=;J^?hb?@l(v87KyP zpYo+Xc*eqGr4RJlq^>uDEMd#{uC@3#AvT0U#~@BMr4-GA?Q`mTM{Ns2^!M Y7R)H0e!8woZD~DYtoYN!a~2E?09A9X7XSbN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..98b93a45119894fd26e2761b52c4f6a80672bda0 GIT binary patch literal 1277 zcmb2|=3oGW|Gl#h<|(_1FjOzAKISRV#LTYpuf;h1NT+nyoHrY?A0C-#EAxQ+N6sUM zo4Y^$xVkyT_2ZOfyRW=m=hFG!Yp3qp4L_?_EuHeX`0KAdmUU~NuAYDG^RMff^*7$k z^Zxm5`)wn;obvv9nRi|<|E)~5IP#<5MEcXnU)u$zBER;{SL^UiP8@7g|F-Wg1+u9rGF^Mq`g$DMSZ8K3{{ zuoLrn!gpDHM#kMlrE)O&eOf_#=QkUlC-?l6EroX;c6n!Tai09NJEx6=*IN5Lv76)e zX(LGK`NJ-+?bNP8%s&pYYg)ph()OQ+_U^Y4-u1nB-=QlkmzkES9b478F)L}!G>O%! z$FjVWub%$-<^B81db4luy`LODL+{`hPBmko);1PnHlOJQ@4I$`Y@7(Ri4Sb~9(|?n z`*u{%pWzQgzGs%JJqs58yt4D>mLoM$Np`x1^QX-?KLLdJeYS)rF51f_{5Et3$k+11 z$i{$8jN`JGw@!XutpCoS%Ba3EKi|BQ_@>6}yWK;e#OK!E8_s|o z(zErzorh~!#orw^65e^eCET~-YD>6x#a7kz+CEQMh1Y5VU7GyY=({3N+uu#!je5SG zzLU5n{+XQ`m;%X_OT3=GbNc1*^fmFJAVDaxyQMn(j_k2tsqYM~=>r{5H?una&R+d@ za4qw{cNl-$-LrH4{BseH&;0$fuRl5gsMex5|LEQ5!g7i1IXn3|!PH@(e60phXRk4w zFMCWX{oT|5e-nGNfnG%-(3I2y0|2NJWN!P$V>7MO7nz;|y0Y*YrfDEG@&8J!0Mkl+P3St_O?`q`=tg4|2*GxKE$&O+=D~3&X8# zM|I+NLsaAb{)p%V*`o>!iyc!zMu$DRrwVr^I1aueq6TcfDXI~B(Uc>_Bv^SeO0@ia zUKbsv|N5=T`O9}d{+@lQF3#TfY5VfCpTF#g|8MvI#};duf4l1M*xKYQKfI@U9@ho) z^cSbO@6Xk*xsz*uXMe3+{>xXNP3mWT`~5XNe7{|MZ2q_9)A!r&-!tEC4{y2s`)~d8 z-<`Uv|NHLgE%VJUKU{X@ZJJ*0z5kZ7_Q8e5w&!O*ZryIG^Pef|P*tCY1p@;BDWIXR literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..889b9c947e0216d0c77d9b11a1301957ca1f2d67 GIT binary patch literal 1235 zcmb2|=3oGW|5In3&69Q)U`wxF-ae^`!EwU!la}f7#oRnmHyK(|Jq2cV&sXS|J^Af; z>(9$yBO4SPbk3B#4GI=ox%*mP^!@jC`o-CA-xh!VRr&c_X`cPxjkY=eW8%K;UVi*q z-OtzCdinYNzsr9wU+eq-tnT;rw~3Xh@z-a}pZ_f?>2E@s(F}tbHxqUpTyn6dX(ms( z;ndwTPSskyJL23}=vO<`$XV%|e?qR+B(k6UHoCEq3?eA^ArQP$oc-y(w zVr9Se=UKnsEU-`3n{$4%;rX13i&mGn|9uhj_Lx=Xn#s#wANgtZxMZ$HeEOVq#_@Lb zvkqFkZUOnbr&29>-pLv6XJQNz&j=s00TT8G{-wO>$HS#LJOvw7v7U55VV z&t4w0xjRF>xAGE%aKF3q-R>E$_VFLimWVt2L8>i0Tl`i@>q6sKYkb3JfPzA`?VQyo zA?I}2%FdO0)RLbWr13$BG{dW(1AI#NAGxs8>)mU~vpjS6KGADC7gr?ioUXsK{P1e& zxVs--wS}ASf!YPMwXJaO(U9|oh9&1F@0PH7elnJ4?q}m2-3yEB*vf%|=N|xVtNb)e zs=9Mw@w`1pLgFSr6m1Kah)b+!eW=jh&IS7^%K`C1k7 zRIhE86Hrj|iK!&mHn6oIK3rnoyv{QL!6#yQOk2UpV1@OQd(ykR7rvWNE%h$5V(G)F zZQ)XJnH9W;y?M5F-Z>gD8>kk@h`wCC;!MD7A&>}^_$tSaq+-vZ4_t?}dDa{3IReuI z*9>Cpl?FP@7wC{%E?~yIwrY?)Pzw)#5OU5{0{T!5=)+tkpbzc!fYz}AL)C2V-s!ci z3(a^7uK#`pl?N&O1hwuEs#dTW@%3Rd7n)7nEs-U>6KDd2vHamYh~GeVUPp81-?z!k zAa_X@0PVlk0MvPz?T-3ku-_s6_}YEvFi`R@$d&p)SN>H3CUaDN9?-dUK+9f<18uFF z2nFwKhPru$~e)@4@`3>FQ`qQ`n<-g4HWX5B2 G1_l5FkbP?a literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b29a9e34953abd5c3e6064dc99a91cd82bbd98e0 GIT binary patch literal 1613 zcmah}eK4B`7}u^JDO!iqMAp`=Zr8hAE0od@?AqcC zKF;TIYnNgZsx%)Jw32K@xM`cyBuJw|HE&BAFO8SIZ;Gb&NAuT{-}5}b-|u;z_kCTM zC@b@C?Ru@6tUYreMA{g7_G6m${&`!1l}~){Ui5EHC?Lu!g7a6jVqaIwP16p7Tj?do z#bh5BsIjc;v#w95$32su$(nBI2lzHs-~;6^cwJ7ZcqtYLNCp-{0+#|$DPq#KDrrF? zZ=sp&140A*sYaO-G!0Zq{vN@HSCHs!WiuhhC95Dku0!{x~vUs zy4N4y?Ngq_MOPQ`{~gT_a6aNUd`%KhT0k)#vJQDJr|f2F=T^6~&bX)SUf58V}t>pNvuystWoVX(ZTVf$M3cfbjIzcp~E>Efm&!P!d4XtF{$zU;0_8Nu9v`isRv-; z^#B%zGgEF1Y=}8%^(=cHuo1M+Uvw9T#zy-*J&DV8EI z^8)PRh``29lq!hB!c1|z=rHiDnK*`923`zdylmcrtxCWF_qydjg z4QT@v!j%(g&FM1*&+m0oMQ!Rd$lL24a2-u>Aw(pj4bQ+{Hd>L2JGyYJ- ze_kCTTlaWPN=FL5GA)Fe6PV0a>1G0q4z`ipT7I-h?|HVVECVY96vl&Pm>{KE{@ z%e&lZG_a=Tju9UiMqK2@T*2%hcN#}D0rc*;rSHd@TLWu4?#$P!gV7Pny}4nz%U+4f zR&qv$wEiP9O?{an^CM2c-_3)J#q;=at6Xh#<{*8BmQD4M%|XvHTgaz6_DIVk*)D&S z4M(C^Jg?0tr5Z9_K%5bB0RLj}o{0OxCj<;ezglg#5!QZn(wms3ee@P%pddH bo29ALzJK#k;abo2wfDHV^YzVsR#yK3`wwT1 literal 0 HcmV?d00001 diff --git a/pgame-plugin/src/main/resources/speedbuilders/speedbuilders_template.nbt b/pgame-plugin/src/main/resources/speedbuilders/speedbuilders_template.nbt deleted file mode 100644 index 7d06b878ae9a16f186482484de8fce3fba24fb0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1927 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 literal 1508 zcmZ9Le^8Ql9LL?xF0`{Xv6&{hwxS;{%_Oa%xtT?^vD|e^7K!2|t{^Q3X^7r9Ma{I< zu96_Vva?Mu(kOnI*odYpO-(L^J&F<~Q&1%G@dt+cKH%B5=db7ae!uVc>-+w^?+GkL zxjCOX1@!C$l(3>ezq8U3cen4ex|^jp{$}_bIep~E7q`DkJh!RhllY6g3*@iwNJ()U z;o^opoUqb+A8>I`j*mpnJc%t+cgyud7jO}RYu_l zyfY&4{+;D5Vj~w*Mk5%sHrpm^i@4fqPLH%t%?>)(4AYmh4O*w9-yMH70;V@x&8+6N zI>ZFX_5$zE?Hu+G|*IEipnm#6drg436Rdkp!5k*Tv(b9SwJA&J@VKt>(hy1+8-)&$HoWZX-@Q4!TzXG5=i1{umK_%JX&BkiPAfYClXbyW% z1)*~W-(v%-K5t5~2rAxFU8&JzEJ(YLy9Bx0{-2-&|Iz?=7a3%=pqo-4-1MA+m?Fo5=^MFS(Pv$<7IR_Blu9 z`+v9bGl30^eUHKBfyR3w#+4Fc$^0xc%n>#sQdl`}bZORWZjdi|T4;L8)X5TTUJxdY zpkft)k@csXC%M?JV&~~=m6wLd5w@~P|5$WkO98MDJJ}fc%UwViHI}X!+ssBy^ri(Nf6g(JoLbOaQJ5%ld? zp|&7QkeP<=pg-dOk~y?S=;y@_QB|gPjhKR~Km?Kt6Qo_zD0r=@kgdDDXv?_Qfm6H+D!P(ZC@Ain>UY?zZZ z>(kwOcdS#@0XM!Pzt!;5t{+!V|X<5ZO5>J9A=u;}vEzYs44*gdA;^;DE zO|WHEuZj~o`;^6@d_>zp8#tdRKi>Ip)-cs(ug(AUuZfy|FW>E#nVi-4Q(sId;b+Z< zc#t8pNXpF0r!Xj1mBOBj>XCM;@(;zApB%Yrbm(Wr9Q8|@0R!iqDq=F_^2nIE;kZJo z`Yp$XV;7G{KcV&qJly^T{7iqiE@|luWd|9OG-bf1qT0SBVX`({WqR{=qE;Kyrr+8V ePzML8i3bzxd8dy(oSpV@EV>ApKXt>qx%~q<+a#j^ diff --git a/pgame-plugin/src/main/resources/speedbuilders/well.nbt b/pgame-plugin/src/main/resources/speedbuilders/well.nbt index 5d3e6aa98a6859874291fc16b23b9c4fe65a83ea..f57d7fa496e72dbb1e3cbfd31c2296a6704479e9 100644 GIT binary patch literal 1589 zcmaize=wVO9LKe`u~lhLZ;6RyrQ=${QgB2@I!W_5&=%Q|xV& znRQ#y8?%@|O}vpcfT<#%qMa$H=d!O3CoTx!62kY>*f>;$=x8FJ)uJpDhIJw(gfII7?c zMmVyP?&hhegDd+!=Om=Uf+^Q}G1PWet@jTCc#)o)~m zn;YeH4Vb0a2{i0WmF=r&^8S@sPX?Bkz*Fp7P~#`taR3@Pzb!0vFs^TR3V_fLfvxC? zRa;Zk8&#v*Qs}FA#li#yVBdow_-_NwBe+=71JY=7pli2MI_*Ov+Skf3qX5DSZNSvv ztC4rm?}CjvG{3h4Xd}o=dD_c%NwLtSvbvKwoN?bIVD z98av`gKW|=?6H`r41N+zpr7H~q)4QZ;=Bck2tALACi2|8NvaCgggpAV@bJuv^pvv(}cRF34MrOC(T;b2J-+f&wTwPAB9>xC^S8=xpCU1pb z=cd#Q{PNMVLWP=;%++B69SZYuUAUB*sOi=e)&fP&b&=1|`WhOBH%PHnhOQB1;qNKD z;8Trsv5$(LWhPVfvh`lnNz3bh{pZ+r+LDKT6;7mAUFwh4c825p>D_dxtJDc7#jY5lQ=?K7@=;2qIv&Er^eOzoz7M1sf9yH_`0#l?Ki;3$`*AL^ zw=q8^8BdV{hdAw(pT1m6bNIquIf-vn-QL}({4PaXdnm%cnO3fUwE||JT zWp3US8F^`}(w8W&YZuagWv+XC)7V1UV=um?db2~#q28ZXd>bmypw&U0`Dk$u85$RpoUp-^sZr#JMt|s(?TljSaWE>{|iFZsLlay6NXNM8dJr zws5=nwxX`$v{82?p{fazMd>HwwnUe-PB6A+ZM!m?&7l$Zp4WJGq*Jlv$y|`DjK{-O z)H+DI9W)jY#-j9S}7mRnwEJKIWg_ldEBWzF)!mE2br}<0)xz3f~vKjM1pLVZPil^d;763lJCl zP|@0RfX86sf|CpH(J`&=tC7y#HuT+J2pQ9iSjd` zpSD6@Dh+hdEftu9v|2jhQ0UzU3(t1P-_|TbHX-XNl-7hO#&YsU z^vhr)TW^1=0Cj|IZ@jw}VDv?A)tH%|Wsd{fKbV6Ch!Qye!5nj7Jn_l#i$p&XVxiNP zyf0J02C*w>ahnD0f_gM)Iqeyw-GT)m0l~gzu2Mb=c zYldRf9`{Fu0@an|n6lwhQTvAP+{xETlo=X56`7I$OeTs~&Tzx-iaidMXoj1AEaPSc z6;G^>WM{vhoz#eHI5aQWm_gEbSkqPbxV#xwCEBuR6UynA!$BZO#5H)Gdh3CA;Q zbuY`r%qBJPX5M6cX?%Y6p@0)z)-CZ6WQF$eDc9AwhWhA<^0dSSE`#NKNuqkdjZokD z?X}#S+oYWzU68(IH<(fvgb{P9T9b}_TA4if!sHWX>dzR4n_*r%ZA37jO?w}H!s~MP zKG?SXcHz6626$qg0ZAv|M=kT|BmClqwhf0{a^32UKN%9?(UN+{N3hSogJW*I8sY_J zF`4bAogSJ#nO-RI7w^jV|6w50v|#8-qyl~^G+MKoSN9UPQXenc3_K?ELO u$NL44xP* Date: Tue, 28 Jan 2025 08:16:12 +0100 Subject: [PATCH 12/18] Increase rank booster multipliers --- .../kotlin/info/mester/network/partygames/BoosterManager.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 95f225e..46e63ef 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/BoosterManager.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/BoosterManager.kt @@ -12,13 +12,13 @@ data class Booster( class BoosterManager { private fun getRankBooster(player: Player): Booster? { if (player.hasPermission("group.insane")) { - return Booster("Insane Booster", 1.5) + return Booster("Insane Booster", 1.75) } if (player.hasPermission("group.pro")) { - return Booster("Pro Booster", 1.2) + return Booster("Pro Booster", 1.3) } if (player.hasPermission("group.advanced")) { - return Booster("Advanced Booster", 1.1) + return Booster("Advanced Booster", 1.15) } if (player.hasPermission("group.beginner")) { return Booster("Beginner Booster", 1.05) From 43e6ebed5ebc9fbba7263899870c828f2b79548d Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Mon, 16 Jun 2025 19:57:26 +0200 Subject: [PATCH 13/18] API: /admin skip - instantly ends the current minigame Plugin: Health Shop: - Fixed a bug where you could get an insane amount of points from survival time. - Made supply chests appear less often. - Increased price of double jump to 10 health. Gardening: - Removed from Family Night. - Updated some weights of objects. Mineguessr: - Biomes now use their translated name (multi-language support). - Balanced scores. Speed Builders: - Corner stairs are now properly handled, no more impossible structures. - Incorrect blocks are shown after judging. - Lots of new structures. - Balanced scores. --- .gitattributes | 1 + .../network/partygames/api/Bootstrapper.kt | 16 + .../mester/network/partygames/api/Minigame.kt | 14 +- pgame-plugin/build.gradle.kts | 13 + pgame-plugin/src/config-schema.json | 9 +- .../mester/network/partygames/PartyGames.kt | 2 +- .../network/partygames/PartyListener.kt | 15 +- .../partygames/game/GardeningMinigame.kt | 13 +- .../partygames/game/HealthShopMinigame.kt | 55 ++-- .../partygames/game/MineguessrMinigame.kt | 63 ++-- .../partygames/game/SpeedBuildersMinigame.kt | 282 ++++++++++++++---- .../partygames/game/gardening/Plant.kt | 4 +- .../partygames/game/gardening/ZombieWeed.kt | 22 +- .../game/healthshop/HealthShopUI.kt | 5 +- .../game/healthshop/SupplyChestTimer.kt | 2 +- .../game/snifferhunt/TreasureMap.kt | 1 + .../sidebar/GameSidebarComponent.kt | 4 +- pgame-plugin/src/main/resources/config.yml | 5 + .../src/main/resources/health-shop.yml | 4 +- .../src/main/resources/speed-builders.yml | 36 ++- .../resources/speedbuilders/animal_farm.nbt | Bin 0 -> 2537 bytes .../main/resources/speedbuilders/bordem.nbt | Bin 0 -> 1479 bytes .../main/resources/speedbuilders/botanic.nbt | Bin 0 -> 1406 bytes .../resources/speedbuilders/drug_farm.nbt | Bin 0 -> 1496 bytes .../resources/speedbuilders/herobrine.nbt | Bin 1277 -> 1335 bytes .../main/resources/speedbuilders/jungle.nbt | Bin 0 -> 2283 bytes .../main/resources/speedbuilders/kitchen.nbt | Bin 1613 -> 1645 bytes .../resources/speedbuilders/stronghold.nbt | Bin 0 -> 2356 bytes .../main/resources/speedbuilders/village.nbt | Bin 0 -> 2441 bytes .../resources/speedbuilders/witch_hut.nbt | Bin 0 -> 2290 bytes pgame-plugin/src/speed-builders-schema.json | 31 +- 31 files changed, 433 insertions(+), 164 deletions(-) create mode 100644 .gitattributes create mode 100644 pgame-plugin/src/main/resources/speedbuilders/animal_farm.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/bordem.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/botanic.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/drug_farm.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/jungle.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/stronghold.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/village.nbt create mode 100644 pgame-plugin/src/main/resources/speedbuilders/witch_hut.nbt 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/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt index d23e2f8..144569b 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Bootstrapper.kt @@ -111,6 +111,22 @@ class Bootstrapper : PluginBootstrap { 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", ) diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index 8071ec1..eff23db 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -39,7 +39,7 @@ import kotlin.random.Random abstract class Minigame( protected val game: Game, - val minigameName: String, + minigameName: String, ) { private var _running = false private var countdownUUID = UUID.randomUUID() @@ -98,6 +98,7 @@ abstract class Minigame( private fun end(nextGame: Boolean) { _running = false + stopCountdown() audience.hideBossBar(game.remainingBossBar) finish() @@ -248,15 +249,22 @@ abstract class Minigame( 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 handleRejoin(player: Player) { + } abstract val name: Component abstract val description: Component diff --git a/pgame-plugin/build.gradle.kts b/pgame-plugin/build.gradle.kts index f695027..5cd7994 100644 --- a/pgame-plugin/build.gradle.kts +++ b/pgame-plugin/build.gradle.kts @@ -1,6 +1,9 @@ +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 } @@ -46,6 +49,16 @@ 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) diff --git a/pgame-plugin/src/config-schema.json b/pgame-plugin/src/config-schema.json index 33e7822..805e2ad 100644 --- a/pgame-plugin/src/config-schema.json +++ b/pgame-plugin/src/config-schema.json @@ -103,13 +103,20 @@ }, "replace-config": { "type": "boolean" + }, + "family-night": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ "minigames", "save-interval", "spawn-location", - "mineguessr" + "mineguessr", + "family-night" ], "additionalProperties": false } \ No newline at end of file diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt index 2203053..34f9154 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt @@ -170,7 +170,7 @@ class PartyGames : JavaPlugin() { // register bundles for each minigame, then family night core.gameRegistry.registerBundle( this, - listOf("healthshop", "speedbuilders", "gardening", "damagedealer", "mineguessr"), + config.getStringList("family-night"), "familynight", "Family Night", ) 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 index 30b1019..2a6e76f 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt @@ -7,13 +7,13 @@ 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.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.NamespacedKey import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.entity.ArrowBodyCountChangeEvent @@ -63,6 +63,10 @@ class PartyListener( 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.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") @@ -277,8 +281,9 @@ class PartyListener( val spawnee = event.entity spawnee.setRotation(snappedAngle, 0.0f) spawnee.setAI(false) + spawnee.isSilent = true spawnee.persistentDataContainer.set( - NamespacedKey(plugin, "spawned"), + SpeedBuildersMinigame.SPAWNED_ENTITY_KEY, PersistentDataType.BOOLEAN, true, ) @@ -294,11 +299,7 @@ class PartyListener( return } val target = event.attacked - if (target.persistentDataContainer.has( - NamespacedKey(plugin, "spawned"), - PersistentDataType.BOOLEAN, - ) - ) { + if (target.persistentDataContainer.has(SpeedBuildersMinigame.SPAWNED_ENTITY_KEY, PersistentDataType.BOOLEAN)) { target.remove() } } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt index f0a2279..5ba0251 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt @@ -38,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 @@ -318,8 +317,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), @@ -546,17 +545,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) { diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index 85ea818..ccda14b 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -33,6 +33,7 @@ import org.bukkit.entity.EntityType import org.bukkit.entity.FallingBlock import org.bukkit.entity.Player 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.EntityRegainHealthEvent @@ -137,7 +138,7 @@ class HealthShopMinigame( private val arrowRegenerating = mutableListOf() private var state = HealthShopMinigameState.STARTING - private var fightStartedTime = -1L + private var fightStartedTime = -1 private var readyPlayers = 0 /* @@ -216,9 +217,13 @@ 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 + // 1 point every 10th second if the player is still alive (last player standing, time is up) val survivedPoints = floor((survivedSeconds / 20).toDouble()).toInt() * (if (didSurvive) 2 else 1) if (survivedPoints > 0) { game.addScore(player, survivedPoints, "Survived $survivedSeconds seconds") @@ -264,21 +269,7 @@ class HealthShopMinigame( 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, - ) + resetPerks(player) } // start a 30-second countdown for the shop state startCountdown(30 * 20) { @@ -286,13 +277,19 @@ class HealthShopMinigame( } } + private fun resetPerks(player: Player) { + player.persistentDataContainer.remove(NamespacedKey(plugin, "steal_perk")) + player.persistentDataContainer.remove(NamespacedKey(plugin, "heal_perk")) + player.persistentDataContainer.remove(NamespacedKey(plugin, "double_jump")) + } + private fun startFight() { if (state == HealthShopMinigameState.FIGHT) { return } state = HealthShopMinigameState.FIGHT - fightStartedTime = System.currentTimeMillis() + fightStartedTime = Bukkit.getCurrentTick() for (player in game.onlinePlayers) { // close the shop UI @@ -326,6 +323,7 @@ class HealthShopMinigame( override fun finish() { for (player in game.onlinePlayers) { + resetPerks(player) // give every alive player survive points if (player.gameMode == GameMode.SURVIVAL) { giveSurvivePoints(player, true) @@ -333,6 +331,7 @@ class HealthShopMinigame( // reset max health val attribute = player.getAttribute(Attribute.MAX_HEALTH)!! attribute.baseValue = attribute.defaultValue + player.sendHealthUpdate() } } @@ -639,10 +638,11 @@ class HealthShopMinigame( // check for double jump perk val player = event.player if (player.persistentDataContainer.get( - NamespacedKey(PartyGames.plugin, "double_jump"), + NamespacedKey(plugin, "double_jump"), PersistentDataType.BOOLEAN, ) == true && - player.gameMode != GameMode.CREATIVE + player.gameMode != GameMode.CREATIVE && + state == HealthShopMinigameState.FIGHT ) { // check if the player is on the ground and allow if (player.location.block @@ -691,6 +691,12 @@ class HealthShopMinigame( } } + override fun handleBlockPhysics(event: BlockPhysicsEvent) { + if (event.block.type == Material.OAK_PLANKS) { + event.isCancelled = true + } + } + override fun handlePrePlayerAttack(event: PrePlayerAttackEntityEvent) { if (state == HealthShopMinigameState.FIGHT) { event.isCancelled = false @@ -746,12 +752,11 @@ class HealthShopMinigame( if (player.gameMode == GameMode.CREATIVE) { return } - val perk = - player.persistentDataContainer.get( - NamespacedKey(PartyGames.plugin, "double_jump"), + if (!player.persistentDataContainer.has( + NamespacedKey(plugin, "double_jump"), PersistentDataType.BOOLEAN, ) - if (perk != true) { + ) { return } // check for last double jump 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 index e70497b..64c0d0a 100644 --- 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 @@ -8,6 +8,7 @@ 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 @@ -15,6 +16,7 @@ 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 @@ -43,6 +45,15 @@ private fun levenshteinDistance( 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, @@ -75,7 +86,6 @@ class MineguessrMinigame( private var state = MineguessrState.LOADING private val biomeList = mutableListOf() private val guessed = mutableListOf() - private var selectedBiomeIndex = 0 private fun getRandomChunkAsync(): CompletableFuture { var worldSize = sourceWorld.worldBorder.size @@ -136,22 +146,6 @@ class MineguessrMinigame( "This chunk contains ${biomeList.size} biomes.", ), ) - selectedBiomeIndex = Random.nextInt(0, biomeList.size) - // turn biome text into underscores, "EXAMPLE_BIOME" -> "_______ _____" - val biomeText = - biomeList[selectedBiomeIndex] - .name() - .map { - if (it == '_') { - ' ' - } else { - "_" - } - }.joinToString("") - audience.sendMessage( - MiniMessage.miniMessage().deserialize("Hint for one biome: $biomeText"), - ) - CompletableFuture.completedFuture(null) } } @@ -167,21 +161,15 @@ class MineguessrMinigame( } } - private fun formatBiomeName(biome: Biome): String = - biome - .name() - .split('_') // Split by underscores - .joinToString(" ") { word -> - word.lowercase().replaceFirstChar { it.uppercase() } // Capitalize first letter - } - private fun finishRound() { stopCountdown() // turn biomes into this: "Biome Name, "Biome Name2", "Biome Name3" - val biomeText = biomeList.joinToString(", ") { formatBiomeName(it) } - audience.sendMessage( - MiniMessage.miniMessage().deserialize("The chunk had these biomes: $biomeText"), - ) + 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( @@ -221,13 +209,19 @@ class MineguessrMinigame( } 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.name().replace("_", " ").uppercase() } + val biomes = + biomeList.map { + it.getBiomeName(event.player.locale()).uppercase() + } if (plainText.uppercase() in biomes) { audience.sendMessage( MiniMessage @@ -236,10 +230,10 @@ class MineguessrMinigame( ) val score = when (guessed.size) { - 0 -> 15 - 1 -> 10 - 2 -> 5 - else -> 3 + 0 -> 5 + 1 -> 3 + 2 -> 2 + else -> 1 } game.addScore(event.player, score, "Correct guess") guessed.add(event.player.uniqueId) @@ -253,7 +247,6 @@ class MineguessrMinigame( val minDistance = biomes.minOfOrNull { levenshteinDistance(it, plainText.uppercase()) } ?: Int.MAX_VALUE if (minDistance <= 3) { event.player.sendMessage(Component.text("You are close!", NamedTextColor.YELLOW)) - event.isCancelled = true } } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt index 489fe74..5a0e334 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt @@ -13,10 +13,13 @@ 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 @@ -24,11 +27,14 @@ 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 @@ -67,10 +73,11 @@ 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, @@ -78,8 +85,22 @@ data class StructureData( 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 { @@ -91,17 +112,15 @@ private fun Location.toPlayerArea(): CuboidRegion { 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")) @@ -113,15 +132,36 @@ class SpeedBuildersMinigame( val structureConfig = config.getConfigurationSection("structures.$key")!! val difficulty = structureConfig.getString("difficulty")!! val displayName = structureConfig.getString("display_name", "Unknown")!! - structures.add(StructureData(key, StructureDifficulty.valueOf(difficulty.uppercase()), displayName)) + 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) + } } } @@ -148,8 +188,8 @@ class SpeedBuildersMinigame( throw IllegalStateException("No structure data is selected!") } val structureData = currentStructureData!! - val structureFile = structureData.fileName - return structureManager.loadStructure(File(originalPlugin.dataFolder, "speedbuilders/$structureFile")) + val structurePath = "speedbuilders/${structureData.fileName}" + return structureManager.loadStructure(File(originalPlugin.dataFolder, structurePath)) } private fun selectStructure(difficulty: StructureDifficulty?): StructureData { @@ -163,27 +203,36 @@ class SpeedBuildersMinigame( 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 @@ -200,6 +249,7 @@ class SpeedBuildersMinigame( continue } correctBlocks++ + incorrectBlocks.remove(incorrectBlock) continue } // special case for gates @@ -218,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 @@ -235,7 +304,9 @@ class SpeedBuildersMinigame( continue } } + correctBlocks++ + incorrectBlocks.remove(incorrectBlock) } val originalEntities = original.entities val copyEntities = copy.entities @@ -262,13 +333,14 @@ class SpeedBuildersMinigame( } correctEntities++ } - return (correctBlocks + correctEntities).toDouble() / (originalBlocks.size + originalEntities.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 @@ -339,7 +411,7 @@ class SpeedBuildersMinigame( for (entity in startPos.world.entities.filter { entity -> playerArea.contains(entity.location.toBlockVector()) && entity.persistentDataContainer.has( - NamespacedKey(originalPlugin, "spawned"), + SPAWNED_ENTITY_KEY, PersistentDataType.BOOLEAN, ) }) { @@ -364,6 +436,7 @@ class SpeedBuildersMinigame( private fun giveItemFromBlock( blockState: BlockState, player: Player, + ignoreBisected: Boolean = false, ) { // special code for fire if (blockState.type == Material.FIRE) { @@ -387,10 +460,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) } @@ -398,11 +481,51 @@ 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() + } } } + 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 @@ -424,7 +547,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 } @@ -436,14 +559,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) { @@ -452,8 +577,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 }) { @@ -489,6 +614,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 @@ -566,10 +696,12 @@ class SpeedBuildersMinigame( player: Player, ) { val snappedAngle = snapTo90(player.location.yaw) - event.entity.setRotation(snappedAngle, 0.0f) - event.entity.setAI(false) - event.entity.persistentDataContainer.set( - NamespacedKey(originalPlugin, "spawned"), + val spawnee = event.entity + spawnee.setRotation(snappedAngle, 0.0f) + spawnee.setAI(false) + spawnee.isSilent = true + spawnee.persistentDataContainer.set( + SPAWNED_ENTITY_KEY, PersistentDataType.BOOLEAN, true, ) @@ -581,11 +713,7 @@ class SpeedBuildersMinigame( if (state != SpeedBuildersState.BUILD) { return } - if (!target.persistentDataContainer.has( - NamespacedKey(originalPlugin, "spawned"), - PersistentDataType.BOOLEAN, - ) - ) { + if (!target.persistentDataContainer.has(SPAWNED_ENTITY_KEY, PersistentDataType.BOOLEAN)) { return } val playerArea = playerAreas[player.uniqueId] ?: return @@ -627,6 +755,7 @@ class SpeedBuildersMinigame( ) 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 @@ -643,7 +772,7 @@ 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(10 * 20) { startBuild() @@ -652,19 +781,22 @@ class SpeedBuildersMinigame( 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( - originalPlugin, + plugin, "spawned", ), PersistentDataType.BOOLEAN, @@ -673,14 +805,24 @@ class SpeedBuildersMinigame( entity.remove() } audience.sendMessage(Component.text("Build the structure!", NamedTextColor.GREEN)) - - startCountdown(30 * 20) { + // 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 = @@ -688,17 +830,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), @@ -706,10 +846,32 @@ 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 @@ -722,8 +884,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()) { @@ -731,7 +893,8 @@ 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 @@ -743,6 +906,7 @@ 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) { @@ -751,6 +915,10 @@ class SpeedBuildersMinigame( end() } + override fun onLoad() { + game.world.difficulty = Difficulty.NORMAL + } + override fun start() { super.start() // set up the player area for every player @@ -772,7 +940,7 @@ class SpeedBuildersMinigame( 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 30 seconds to replicate the structure.", + "After the time runs out, you will have a little time to replicate the structure.", NamedTextColor.AQUA, ) } diff --git a/pgame-plugin/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 index 5923802..b95da1a 100644 --- a/pgame-plugin/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 @@ -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/pgame-plugin/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 index f91bdcc..a2f1b9b 100644 --- a/pgame-plugin/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 @@ -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/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 index 56aa8c1..ddfd204 100644 --- 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 @@ -480,8 +480,9 @@ class HealthShopUI( 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, 64)) + val oakPlanks = purchasedItems.firstOrNull { it.key == "oak_planks" } + if (oakPlanks != null) { + player.inventory.addItem(ItemStack.of(Material.OAK_PLANKS, oakPlanks.amount)) } } } diff --git a/pgame-plugin/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 index f1477f1..84ccfce 100644 --- a/pgame-plugin/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,7 +6,7 @@ import java.util.function.Consumer import kotlin.math.exp import kotlin.random.Random -private const val STEEPNESS = 2.2 +private const val STEEPNESS = 2.05 class SupplyChestTimer( private val minigame: HealthShopMinigame, diff --git a/pgame-plugin/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 index 4fe2464..74a7a24 100644 --- a/pgame-plugin/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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt index 9706cba..7540fb1 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt @@ -17,7 +17,7 @@ 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 } @@ -27,7 +27,7 @@ class GameSidebarComponent( 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}", + "${i + 1}# ${if (player.isOnline) player.name else "${player.name}"} - ${playerData.score}", ), ) } diff --git a/pgame-plugin/src/main/resources/config.yml b/pgame-plugin/src/main/resources/config.yml index 2157163..8c59465 100644 --- a/pgame-plugin/src/main/resources/config.yml +++ b/pgame-plugin/src/main/resources/config.yml @@ -51,6 +51,11 @@ minigames: z: 4.0 class: info.mester.network.partygames.game.MineguessrMinigame display-name: Mine Guesser +family-night: + - healthshop + - speedbuilders + - damagedealer + - mineguessr save-interval: 10 spawn-location: ==: org.bukkit.Location diff --git a/pgame-plugin/src/main/resources/health-shop.yml b/pgame-plugin/src/main/resources/health-shop.yml index afb849d..b358963 100644 --- a/pgame-plugin/src/main/resources/health-shop.yml +++ b/pgame-plugin/src/main/resources/health-shop.yml @@ -330,7 +330,7 @@ items: lore: - Gives you the ability to jump twice. - The perk is kept after use and has a 3 second cooldown. - price: 7 + price: 10 slot: 39 category: perk flint_and_steel: @@ -348,7 +348,7 @@ items: lore: - A basic oak plank. - Good for building. - price: 4 + price: 3 slot: 44 amount: 32 spawn-locations: diff --git a/pgame-plugin/src/main/resources/speed-builders.yml b/pgame-plugin/src/main/resources/speed-builders.yml index f8490a6..c45030b 100644 --- a/pgame-plugin/src/main/resources/speed-builders.yml +++ b/pgame-plugin/src/main/resources/speed-builders.yml @@ -24,6 +24,21 @@ structures: 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 @@ -31,9 +46,6 @@ structures: dragon: difficulty: MEDIUM display_name: Dragon - enchanting: - difficulty: MEDIUM - display_name: Enchanting Room bigportal: difficulty: MEDIUM display_name: Big Portal @@ -43,10 +55,19 @@ structures: cart: difficulty: MEDIUM display_name: Cart - colors: + botanic: difficulty: MEDIUM - display_name: Colors + 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 @@ -72,3 +93,8 @@ structures: 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/animal_farm.nbt b/pgame-plugin/src/main/resources/speedbuilders/animal_farm.nbt new file mode 100644 index 0000000000000000000000000000000000000000..ae244c0a58240f0d7462694170b40f1a2864f84f GIT binary patch literal 2537 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
v#avTDLw>s+F>LivomI} z{UX*^5h|z(Y;4MWJvlt4qRmx%BO;b4h2;a41SD9)mfGzQtCq*_s@ojw;T9N<+X-DK zc<#C?32=&Z$z<^#u+b#}J(}7Ib)vC*vT*KYWmmK!szDNsZT_{=o z`3cBbi>wCyc?;C^##f3p1%@UB2=Htno_#HeNcsoLw`p%h)6vcbd1jC)INSeeR2=@d zbDdS=;`3Nv6B`P0d)0+V!o@ChQUVtv->-KW9~)+rL&}F4Bkj$Qg!pJHr`4d_8`mfl zviJy87Yq$n7%g*$83@}{G7rcPG4MvWVTqiBN#asns(k2AbCv*z0qYND9dEc<=I5)x zE^hBD65JTF_{EKgZ6w|<QyLC9&&_F)V%Otc)S7_ed3V`E1 zR9X;F3L59Y;p{Z(!Knq5a%uigsd1P+s0{_qWs6%aDub?;yjrGvHZ@blKbu>rfJI)X))eElt$ zXwnwGU^_<#7wj!5`MdlP5+TSf1cHf!48!OyAN+tj^0ZXELy;<1UL4D>W?1wm`l7UK z@7pvvg}u_hp{l`Cm2iK;$ji1LXHCgwr)HK+61{OD1|>}4 zo7d$zrrDvoM=FM80H2p0{c93SyAVT6#Z=dMuvdQ}o76q(vG}}c3eP)sO_L*fuaS1J zor9jgmT_QEn(ON)e-Nc$OD{jjno~~sG#UimDZR;pU^x#v^c+>AKG4^I?w92vq$Hiz z@UOA5l$l^*=IpIm-E)~DH2cfSXhYgFHXI8ga-5mFysRnvhb8g4A9;nnN)(MZ4{-6~ z4kV*jzZ``N@A|%sx6x>ytyr?Ggw~>jDi?uy-N@{byqaZpNnTZkCtAogqQ^3H*wQ*l i@NjeWC)}+yK3$q{x6`4aa30JId#H1mb@OgfCbC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8f264bcf8c4d29e3465efc4210488919b161a416 GIT binary patch literal 1406 zcmb2|=3oGW|8r-b&AaU;;@0lWr>b2#(e>Bn7tUL5uiLG+>(x2|iN;NwmzQ{5;(eq3 zB01de!^CYz?-U0zHzmHzcqZ38>+$_qj_`#NvzJ@(E85&Qdw%=d&sw>Af5UvQ&i`s) zRx5t!_x^QGKR3SRFMq7p^5(wHlEPKXew~?j{e=JD6{^$B(~Q`KWqsZyaIsI8@yRrp zc{sM`sot@o^Uvna*m&n?#JeP&?=K3(lXrG+yz_XEu<^9r5~qPG_QlAb0n63g>y!iu z0JZ4L02P-TPOCP2e*`Q5)TQ1IQ9lpKxJwU#ih**|Kl_2rC^uYxXY=XL2Gy^>imFNa zl&le!ef^}bwCK21Yx3Mf8~uvbOulxlbNbn{_u`V{6XDK%{85Y7?Rr1Z=1)S&m7N>+ z7#%-z(XhDWjEt-BnP(9c0pqc#x^T&4GsLwMn@@-y@MF z15|tLqNP5N`!2EWJddd!JJ9SMGIx#!JT59YFY_)k0m!!insUE+0?-1PoyQNnGu!}X z0d-FNUDRvf+^hWk=`o&Kf0uWsU*F4P1Q|K8TJXkRjyE7{-kp31rjk|^KAFb^^uw`< zaP445`-1tY#?HOk2V!|l*D-*sXS!nyj9G}|Ad215_^*AIpc{i?aCOlRRQ}5XaiBY) zegQgI;ET0)+KNI>kVl0;YE?nL1-TLKSUFi#{<`8upl?7*_RR@C6Y%)N-U2jBkStw( z097;CRK*wh!7~;foB9CA3vUAYFoyFTFiY^5_JN#n0?b&9#{YV20cAdYzMU83%v@0L z<}rg@B~8$)^Q;jL0=W#IPeI{ajT*eaAJvJPEnl^E`O159?f2bld-VO)K0f<9=CQRB zs~&%OeV_lTf$dv+zOxVWc$oG@H_Torb^P!1|2uED-j#Q*U)8>HftuIw&VXzPhH_}zpX#}4FCRm z#h)erj)Z4d_bq?;FyQz1KV_A#Hp=b#`Hx@Uo@=a literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c72fc3d8c02db3db782bd786142590339da79772 GIT binary patch literal 1496 zcmaJ>X;4#F6m}Tt*p{VKI|Le_&{3+kfe;ZQBrQf|Y6D?PkyL>wp+UfnNQjV$R0;_+ z2r?+6fH-t2TN#$tNJ0oofg~*mp)6@3Y(j>VRyh(T_WD^GrT zo7|>L5M+f-#xZ)y3nV49KPx0>+Wf_d?03pPjgXWF$dq*na6@+VSi&~{iKZPNEb)y$UW4}7$-#pGP|fya(S*( zcXXZxBV$tVMvp%D+_(=;mTm%s(=ZGK(*bfG@wG%+t8tHxoKTj1^LK2%W+$)r`fwRK zDKy{LQ(h8&?$Iq%?$T_!{JLEw)4RX@M|3#%DWT#LSNLwY|II(zFAZ;ZAv3*&?ON7p zv15MnQMvy&lY|}g#R^oCeHWGUk!*y7EtVJ*Oq!t(A=ehgM~EA>7eE`EypYyF%h5(s zHvuoJahc9(GKA#``LT5`YC-N=Ba5LOKanXxwHS*hF+51RQ{;#@-i#j?RR!u~(q7yR zdYP75puzCcgJxef(Bo>ynYJFejXqP|73QTN96*-n=X;HsUfxvC#t!Q-vX=2KstV>~ zmGij=1DC&=0?X)%u5k=4caH)ziehiZC~~#6~))h@71(oR)99GiSo1|I9Z!N&emo&%VME2Q8_R#aY-pwcOa0L6fr z`VA`<9~`k2TrmYpI_m+CA1u#O9w-QSb(fOk0VG_?RS>LGpGo;#Q>J~8Wq&s4+!k)y z7uJ4gGR7ZPMgh+Vagd8;g@~L0P$;Rl-PHM|5Ihd#40b&EFR<`lW%tS;nHgp(=rmFN z7F_-qcy!LqR>ohakRbsYo71`r`u~pr;_Q8`Xz*+M|G!jJw6UwswS*K9vS?UcEoJ3A z@HL)YUA{aaj3iyenO0PE9GRBtWvRl%T_%QdRS<@68k$Wuui1C^o#!*q6v_l|afxJ6 z+6}b1sN_Q|)4+uL*gk%ft?uH3=pHF#IW6UPLWdK~%ap&Qe-eE8q*QKGz`^+9NFp)I z-?l(^ls2Wxp6gGz?NxBlKwg)3K*|*Pn=R}lMX01Dsb!SQ=9F-pR%TmAmLr6g#x;G& zMSWmrLe1=bOFbU^D}TOIX*Pd7U!QimBfX5r$l&;Wg3CD0I47AOQ_@NYxR16|N>?pW eG}-ELRS30t>|TcJipF)j*}9}%KBzsgWy`-A@FB4P literal 0 HcmV?d00001 diff --git a/pgame-plugin/src/main/resources/speedbuilders/herobrine.nbt b/pgame-plugin/src/main/resources/speedbuilders/herobrine.nbt index 98b93a45119894fd26e2761b52c4f6a80672bda0..7cb5ddddc99c7b071b293a9d7a89f8bef5d0c59e 100644 GIT binary patch literal 1335 zcmb2|=3oGW|Gl&Q{U3*kxUau2&mOdOuB*86+37EKyX!e7 z{<|&T#Ep;KDV`|cRrBb3-@Vh;3JKX|xv97Q-c2{QuFdP4AHID3&3!+2&fb0hSK%#Q z_RakJKfEMbM}GZ`w7Ho~K1Ood>@$QURKFYhsJHmqMg%`;R17zDwF; ztA8x|yV-_!Nj4x&)pDKBgPD4^zf0Oz2uk+_Xd^+MA}a_ZY)YzBO|ez<(O9E z##6TNm2>I1MxpxI7DH^5HdDd)S2W`>`8fmq6^jbF|`JReZ&} zlQ(QxpG{`^e6sQ9lLIwH33iqS^Mz*kbDmkw0@V6>W#i8)2WpN~REPF#4?hZ`fPN{? zXNI^95(Gd2pjT$U{GFWkqQ>umUfa3nEbo#)%zG?%j$Zib1LCNa8Vui|M#IA_IF)Ai*N=e>KjF9QX^75$?1j3BB? zzk6Zb#N86F#CL873lxbv|5XAj*#0>@ZAHA~oudK!jeu6vr55Bfz3bfwRtwjpp8P%K zjb7U?*LO)P;-Bn$BbfFgeJ1z!vbi7cOfQ}#nI*Gxdc$ssESa^{hGqK4fQkyA!~uN@ zr1aXP+}~Zj5zAxR2ejhx2{_~Kzx(o6b})eyv4Uhk{yPSABG4>2V>#GR1pl?q5|EPf zwU+fWF%*MzRr8{nQY~E|?%b;k^r!{U$3TYh-*b~Bvtn3*B0IQ1c8lJbjb;gkr7;Vj z?$UoQVhl7$9~6R6^FR!65JULivw)!l^f27#AV%J@{l1B9QrDY+o{QlI$?5JeM>E48 zjeotm8QmCEgH3^{1zAPz0eHxQ8UFHMuiplF?hYF$fX%>(3ONdF!2yqm5?glEDB255 zgvjdmg3Upw--{aBf1lUo&R(8(|9tU&Tif!~r_R*mPhZV5pEv*C4jbEV z^W)og&ustrE;0Ld{PBk?<)6pe$41_JpQio!$DP|hx8B!XzxRIc-`U|)?E9m_-tYc< zMLzz1?aQ@4Kc2eu`SQ<`m&(^qu;M-c`S7O?20hQ)m)Aep@rwWRyOQ_Qdz1e&Ifg}h J@3mlH003}=yqN$1 literal 1277 zcmb2|=3oGW|Gl#h<|(_1FjOzAKISRV#LTYpuf;h1NT+nyoHrY?A0C-#EAxQ+N6sUM zo4Y^$xVkyT_2ZOfyRW=m=hFG!Yp3qp4L_?_EuHeX`0KAdmUU~NuAYDG^RMff^*7$k z^Zxm5`)wn;obvv9nRi|<|E)~5IP#<5MEcXnU)u$zBER;{SL^UiP8@7g|F-Wg1+u9rGF^Mq`g$DMSZ8K3{{ zuoLrn!gpDHM#kMlrE)O&eOf_#=QkUlC-?l6EroX;c6n!Tai09NJEx6=*IN5Lv76)e zX(LGK`NJ-+?bNP8%s&pYYg)ph()OQ+_U^Y4-u1nB-=QlkmzkES9b478F)L}!G>O%! z$FjVWub%$-<^B81db4luy`LODL+{`hPBmko);1PnHlOJQ@4I$`Y@7(Ri4Sb~9(|?n z`*u{%pWzQgzGs%JJqs58yt4D>mLoM$Np`x1^QX-?KLLdJeYS)rF51f_{5Et3$k+11 z$i{$8jN`JGw@!XutpCoS%Ba3EKi|BQ_@>6}yWK;e#OK!E8_s|o z(zErzorh~!#orw^65e^eCET~-YD>6x#a7kz+CEQMh1Y5VU7GyY=({3N+uu#!je5SG zzLU5n{+XQ`m;%X_OT3=GbNc1*^fmFJAVDaxyQMn(j_k2tsqYM~=>r{5H?una&R+d@ za4qw{cNl-$-LrH4{BseH&;0$fuRl5gsMex5|LEQ5!g7i1IXn3|!PH@(e60phXRk4w zFMCWX{oT|5e-nGNfnG%-(3I2y0|2NJWN!P$V>7MO7nz;|y0Y*YrfDEG@&8J!0Mkl+P3St_O?`q`=tg4|2*GxKE$&O+=D~3&X8# zM|I+NLsaAb{)p%V*`o>!iyc!zMu$DRrwVr^I1aueq6TcfDXI~B(Uc>_Bv^SeO0@ia zUKbsv|N5=T`O9}d{+@lQF3#TfY5VfCpTF#g|8MvI#};duf4l1M*xKYQKfI@U9@ho) z^cSbO@6Xk*xsz*uXMe3+{>xXNP3mWT`~5XNe7{|MZ2q_9)A!r&-!tEC4{y2s`)~d8 z-<`Uv|NHLgE%VJUKU{X@ZJJ*0z5kZ7_Q8e5w&!O*ZryIG^Pef|P*tCY1p@;BDWIXR 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 0000000000000000000000000000000000000000..6ce8ebfc7c507812c1381562c413df50e93cbb25 GIT binary patch literal 2283 zcmY*Xdpy(o8}Iz&=+$L9XZej^hH@fu+M4qh-rk=NRAMm z69Y1)R1)=HA=c+|i$Q;(PH;2`UHAYmp2~RmVxW24`8wk+Mqh*Et7#`8t@T}2`KBZd z6<{;6Z=f)k6vkcheZx$f2UuMINV6K7xVzZmb7*Pr@f*gG$yP^OW2er)!3$MkLf(6Y zQKdC}v2w@WT~J?r*`K=agMu&6CkGGLp2AfU^UsZ0LaUaxGagL$APcodG00RcC>pt) zkayvY;Q&@NyuDE)2Ppa!K-csd?y};5ss^W*8b$Ev6VZ_qc{a9jNDvRsev;-ycrn2H zJN!sPe-Td;I*W+1|JKIb_Kg)MtqVSmkV*9HyP%)LbfpS1yjHO0W~H`16}E?@(>(jg ze7caL=}v{U!2u?Pq>pPy1LRjGB`|>@9n4#PA0+CA`Vf$DNGrru3@HCQ`|&lh69EZY zZGp2(DETq3ul!8lM5KqTj?QHb+=}?*Zu&T^zazls+>ST~dwi{+`g{W=@SWazz}EDy zk!kw|@~@5H;qd^k=3~&bCeQy&90-7S4`61q!7u>rXO68EAf$tZ&#wpewy^imR%QG@cs9I)MMWWBt7I= z-(*G)_)zPyQ0v(M)Pp=FxSAT*Krly{S}(xcF<&Okt%fi6HubogA9LFY-DTP+XQq?x zWQ=RmCZ6!NMZL~4WN+4~e!^R$_H_G7RrON?D(nhSov^nE@em@(!Zo@jA;3kepf#yjCx8*)xr! z`y?ET>%j2u)P`;b+Q*MN4q$cEUuyCGYVio^G)BFZFPGrA@v8(-_Z2lbznYw2Aft5$ zpc++Ek=n3PK!){YurUMvCtTd~Ex0x72nwJ#sNbu8>^ITe9Y_bA?bNm#ezN^bIs%A5GzQ^{T$A8T-1Xr$PC+)pZNYO7U>F(Obs_pF?#EeLLM0q$;;(N;9Tq zR^VbDD^=fcyw`Oiq%yluh84CW(sm!l7tuRLQQ?vG;i0_?A=CyGxi))^$7Wvd{ z+o7Knhy7N~6o)Gm3h!e#WI?6F0p7_DHCxXf%c^OcV42%?2)o9JIhjU?ELrB@3kOSV z_oEN7m?xKF*XvCPjGJDn;{Rw9hF{raRQOUiCp9S<)tE6Zu*B)@fLV_|HNo1NpQTEAz%~v$D>LvtF*m#a15y|@S5sJ3ZQB);-XwRX zL*J&<9qYPp?dkuvje3!8L|>dK;49p4tZTXovI*_rZ58UR$~$@MMeH_+43XX6J=SIy*h23=oJlD+;PMbR zygU@5*B87qdp#XyiaS+1H}cIH_K9+#_?^zyxp@-W^eamSVm9Z$U2mf zZuwkEPAI;T-&`^0>{vl~O7{Oa7Fo~zFx0$%e`Fn}@C5$K4c$W%#8xfQkZ?8_OK?ru z8j*B${OLk(?c$B*%B}vK4@z9heDzF9kn|3Lx_HEWGHenaJ{yK<=fqudLCFN>i(xPh zDa}UVgA?Is`~=R;NBrJ-ZfNXCmCKcvW1HQ~gFMp2=e_EO-hUL}lHPT^tWQZ$W*?uq zKD9Uqj1+QrnOU#8Sf1E1N-!a1%^?2&T5j1x8bk;otb((${?&g jTO~ee5@A$e>bga@*2*AsLRQ?j^vx#T9pn$V6)XM^eg~VC literal 0 HcmV?d00001 diff --git a/pgame-plugin/src/main/resources/speedbuilders/kitchen.nbt b/pgame-plugin/src/main/resources/speedbuilders/kitchen.nbt index b29a9e34953abd5c3e6064dc99a91cd82bbd98e0..798570bbc6f3d8b8f904c057f76f543954aebf79 100644 GIT binary patch literal 1645 zcmbtTe^8Ql7`L{TOLrb~7o;Y;u(|G>X2$%9UYS|TiFH$}1*FUxS}KyE64*73nyEE& z!8AH=S7(;dk5hzqQG|M#Xq{GC@`8qFib9>@TcWY=`>M0K?XSImJwHFs=lOh}mssTJ zfPMCh6>z$Hlt=Xbh|!?486QT4ec*EU6MUw6N%dmY%9~%ed+oaUj10M69Cdxw5Vkn2 z^Mb5iW)Ijwl5c-u5K{t7!P|STRzjI?8{TXTUfA>PD;3B2=WEx4n7j zlt?GT1A?8w=cdZ!$&T@_NTDTX4ni9uV~83rT#TFQ2V9J&>N8vnYwgwoh|o&YoZ+o| zxYL~M!}b3a_>|&IJX2nDV}e4_G@dg0HfI5siYyJllA-glLEKCaParzEqLqe~`mNG7 z0jLsfXD-%PbOZHMusNsdEXJfS_F-d0BxGbtw7enC3+ET}20%#Cxhg+x$D z#?P*K7%HkX+d@TN$F`D0KF3-}B7(GI?=Ox>V_SpYfS80B!#Mo{|2f_~w?0XTj_dza z35d^70j$%CeGt}4F~(tTJ=&niD2dK7;^?^riJt7Xs75j#XE=*9oIo4v!jrFBam?CM zLYf{BZy(yv*!QxMs-iicA6wHryR%Iy*a0xL$~a$ zX7sN-*I&rw`acfJ!bcX9=8eH|oGddmT>rsR*@s{@?{sD@tS^&_x0Dv27_iS+{UvmJs4U5r1 z%QC6psOAQu26~}?V|w0cmOL_4nS9(W+MEjxF4Y5w+Gp>Ix2m4+a~Fu1z6c%Wgs$}cQQoJ07|H*fx*vVD_ozvt z!A;C=i5fiI0s!>io0mc>w%r=DKC=@G%UhpcIjx`u(zpzn%=Dm*$V^l-J<{>Zh_Kq{B(polxwek~T}j z_b8^$r5>3c?5sQdWIC_syV2?1@ezFIt$a$ote6pz4O<`7(#PSP%T*KV6A6AM)oMSu zFNOH6pZmtHNwq73N<9Ew@-$CbC+nrpLt-@NT*naR6JeL>AiO?4BEocq^;CKZZ?t;~V%X9J6%sOjQV?SYd@^d<(hWb?o78XzExZ`{Mt?{qGd#ct z#s|D%YoHoA+Zd1Eh+Ox`sD{{yaF!!KUau=0%8Ll4PrB>Vr!M!G5$`4Sza2VaUB1H^ zW;RqPwTzb)elX9`BIGmrGGfhjIu|w5!VRPakr@XdZ#{7znAePTjkinRoO)g;*VBH=}2(h-5jH41ce%y3T<;$C`G{W(58SgA6t Sd}GJGCVD= zKF;TIYnNgZsx%)Jw32K@xM`cyBuJw|HE&BAFO8SIZ;Gb&NAuT{-}5}b-|u;z_kCTM zC@b@C?Ru@6tUYreMA{g7_G6m${&`!1l}~){Ui5EHC?Lu!g7a6jVqaIwP16p7Tj?do z#bh5BsIjc;v#w95$32su$(nBI2lzHs-~;6^cwJ7ZcqtYLNCp-{0+#|$DPq#KDrrF? zZ=sp&140A*sYaO-G!0Zq{vN@HSCHs!WiuhhC95Dku0!{x~vUs zy4N4y?Ngq_MOPQ`{~gT_a6aNUd`%KhT0k)#vJQDJr|f2F=T^6~&bX)SUf58V}t>pNvuystWoVX(ZTVf$M3cfbjIzcp~E>Efm&!P!d4XtF{$zU;0_8Nu9v`isRv-; z^#B%zGgEF1Y=}8%^(=cHuo1M+Uvw9T#zy-*J&DV8EI z^8)PRh``29lq!hB!c1|z=rHiDnK*`923`zdylmcrtxCWF_qydjg z4QT@v!j%(g&FM1*&+m0oMQ!Rd$lL24a2-u>Aw(pj4bQ+{Hd>L2JGyYJ- ze_kCTTlaWPN=FL5GA)Fe6PV0a>1G0q4z`ipT7I-h?|HVVECVY96vl&Pm>{KE{@ z%e&lZG_a=Tju9UiMqK2@T*2%hcN#}D0rc*;rSHd@TLWu4?#$P!gV7Pny}4nz%U+4f zR&qv$wEiP9O?{an^CM2c-_3)J#q;=at6Xh#<{*8BmQD4M%|XvHTgaz6_DIVk*)D&S z4M(C^Jg?0tr5Z9_K%5bB0RLj}o{0OxCj<;ezglg#5!QZn(wms3ee@P%pddH bo29ALzJK#k;abo2wfDHV^YzVsR#yK3`wwT1 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 0000000000000000000000000000000000000000..6d066cad7d43189b85c4fdc83c161a9909c36720 GIT binary patch literal 2356 zcma)5dpJ~iAFpja>P>7V%uFL&TeeArdK-=C@(zX4>^qEH#W{Px}1x=DVm zj;CMp*h@t;X3ft4J+y&m#E$($AgN*7W_tR2M#ev|63q8BBVvAcpK$gK+mPQ<@QAgm zdGoeg-+iNeGqK$FY#vsNc-iN}n^@GYYNCzGR%M_3I-KFkgrcZwm9&3rZXQd}ED~2u zFNqR9dOK92e=D8CFI^gny&$veOt6(nn-;u?W-3H-{Z)*w~xf`3= zfUS`uqG4~Oe(U$CEF+~hYMEbYlm`35o-geLxPd&t%lD;ZWzR<0(y zDG#XM%62EPsq5iQV1ETa3c*mLldRyGX z+_ZA^kb)n~$2GXUR^hb@ZW}K3AlOFdl~9bg^KOW0i{o5??AthvWQIqd{DES}(`Tlg z_KSASFo!(EP5%mSYBHEQz36_Ze>vcpV>*MWl}CE}_5jM!u6z9;o%o7&a4Qws1Lva* zr4#636H{7i7-PlIx##-ec{_70zDTV)tHG@FL;=;VnbMcdooqFRbXqp&howxab9B(x9^om9*Ax*5qAYSzm|Fh1b0po~_|9n3#p6k)rS*4Xrg8}xd1p1z51bLM z%Vff4j5oglIqv3q!sCIetkp^K&a?)_im_Otw~X|MSWO%x>56T{sr41e0GSF=l$hev zU*d5uS#bMoXhb^6yUZD79*eOQyk)-sB{p0ec{nhPOd`1Dj>@M0<>R=Oyej><53fJ| z;;EYSWfg|SeCXOF9cs@A4oqZ4IZB!orv{9k5(71NynGqw)^XcNwP8LFsh-!FM1MYV zAt#;cl0+Koy!S~9Sue?+!(f_|3$jMhiQ3)$*+>hY|EQml5E34LI8#?s!~8p4;_0h; z*Pn$?GoS1y9A=|9$2h-R#8fkqQwV?J-pJgC{|ch#wRwT!I5pMor^Z1+T-L+DAntt5 zXl!BncFV543e{&FMx`1yxOW1vFpD^@d$Mq$Rn!{zB3!R#A;gbTw6rXpFKYGk3vi-M zHHm)S*^i9aPtFO11%%nDV+SA^Z&B*mfDAJMiBmb#wc9i4qC zjAXBb(ZQ>Hk6sJwL$R|73$cIV6>&lLS!mkbcR%v;lOMaDe3<9lKFGy-7KG2;@0l7E z?mN+5WPHlKOWphOlrPnO+t`+4$djG^G@Ot-K5FwiK$W${=H2SDBHcF* ze*2;{CS3cknxTR_&UbB`Cdbspi+u?u9Pn*>JXj-3xWid~shtk;Ca>9gCE1vZ60B#I zns_nsj(}~}g6@rCCDpotC88R`_wiIzIreYYUGyyOIfY&g)C;TS)eTE|1HP52zR7X{3~p@Tx3VmWft>YY@IH zeXy@#GOW(bxsl+djCXRg(H}-8y&jmLO*bZ-4t(YS7Py~Ei|(fVdxu{kzr`f;_xG-3 zJHTM%)ILymdU~jdvks({M9bs#(3*HaxQmjV*ozIWyPFr)%|As>*gQ hjefdgJz`)wH+LgVfLf;cBj!(DiLo1bdVbTU{{o+Q#;^bY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9853dc61cb9c6c82c39ba0667a29dbe9b7042186 GIT binary patch literal 2441 zcmaiydpwhS9LG~>MQ7^l2<4WBiYU3W6e>!QbaE+%SVL*17Bh~V+~#uVI9Y2PoI~b1 zqPb0St7hbqYo;u7-Q2f~>^xJQPW^M9*Z1{$UccY(^ZkC_zt8h1;Ww{adxSL)8UFIt z3Xyqcu*2mL^*(X%%9e8tVp!WB-0W!ll^*{jF4<=E8edTi(n1hbidQ`5!I2{}Xi)tez5?HfX)Runh!!1+0St2*%G|VwDs`%8qeWpWU7Nio=8I#1 zT5E|A`N&~j)Tk0umrHD!V7eij_NL9SUMGP8bfNs17IlL_)fLBia+B(-0dX@Mm}JOS z+`Qa0YFO--Tw1H+V_mjG&rDfZT<`> zx&SQj2B?%x>ufFV`R{GMs<2Q|*x#N3E|TH3gtuGi)YLTyhzP=Cm}?|jeRf|9VyNzG zLThxae)uztCg0DU?^PBheQldasW19JP`eri(Kr4t0V(-U zS^xW$uw!q(i|&8jF9+13f@~iR4UNYrD>*2<%h`jvmdp-tgt8_`;VnAzg!p)5@(Xe@ zjKQ3Wp0U2tyJTuC4Sn;LRwgPrr6c6$A|w8!2JXt? zJ|%L9qZQ0@UC;faPPrjL@V|R9c5csAKH?#Kuc~v*}><-YA`=EP|9ajXt8n`p~ zK098sj?zm>wRZ%rk5Bba2KT~Qh1SwNxg~kKAVfPwFN(-ooM4JOD}Qd zd9-Kyopi5W-YZjnefjR|)8zefX*O4`^YU$SGqVhWZ>r%_{;0~#O3cX?D_z10f@&B_ zg;7=?qx#R3cNM*3lEQQ*xwQ|bZ!_P=X;)(H{aci>?*-2ofN#dfn%L&Na0>kkloLQ% zTDZZ_QlC~>9KwDmRxjr=T&u0}Mq^rIJ0J!**5^n{zASs0v+2;2Y6O4%E>d6MU%r9L zwqD+m(lMmLW4W=&6mkeA)4TtPV!&v`ihD+S!a`fkh8m>lskx+EQVqPMn3Q+;ISaj0 ztwihwAq$Ci=7-y7WiQ(KDULa}A`gXKy~1}?92>IgT<8uBHKqqRL5&Ln#WS(NOCzv$ z!tU-XxHF-m3f7|J`(fct_DcvPdq&8{HtjkGep=rI*+}7l;8KT ztkh*JlE2V0WM&=sTgu|Vv!Ra+m-K?rae}G6p!}#zqdZ^u;lg1-p(4Jp{%$AkA=_Pt zJC&w-w@dO_ewK>Dn7FHUlJbss%KJtJYxOyL2GT>0g)K40DcVSmmMXcCp|JCDj53$Ex+ z*m^sqoGo=o5M<9fHdxo3Se+Sg`?!Qy9u=l0UL8?$&o8kdE#72R4u?BQHQFVxN2D5g zC&|xGw54&TYl>b@jBsj&+-70O%AU(P!OlDlqui4&ZrcUkD1!sl1I~><;ZUy(^2WL| zmi#w)d0jt|@LmdmJXe%66VpG)Ur5INcBL<3>dmDNqKdA<1Mlfo&J-ULCir0MX1cnn zy^3D$aT6bI>O=qzMaMQ-c*9P&E4`;_0?dU3#*olcfheuq-Fa zqub)VAU=w$*@@h@bKF;Qd_!qf$kNKtsF{$CP{JHvq3M*luD*7mTe!V#8^saAhJS(i zBaD7WW^2pUuq^vl$ld5c;tn58xa!k%PWHVySwgQ2cEBKCJ=sb?g2CKS%Eb literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bfec9f98907c252b484ea1cdf7935db09e96d674 GIT binary patch literal 2290 zcmah|2~?8l8ZMJdVxvxq=*?)Iw9!V*1sj)Y+Gf;7E0dVq4V`f}rqIwFvt$%UDsSeF zYpF=+bpxZ&Oe_~{5fDRh|1gpKP~ zpOwb^-smlz#;nkT${mie1;zVbEP{@O&6^b0qPzVmLF9{(Fc2i@i_G#H*^UyaXU6UD zU;cP;uj+ut*f$rDo?RV{zVJqzB?djeb&aD{&r0btd$E8iU4c<&;)6~YLv8Y&FBNd*{A2J zG0D+L9V&T33`?xT62td}jILJI07F{JdpZ!Egj@#^2D};`ntzRX9<+x&FKAnhGsz#u z;97FhY}ABUE2JQ0w*K=PZvB&^u z6Ju6+z@Xm*5~c&PJ8>;<*Wun<#pB<4Vs=-e_BpU?F-8;JnUQ-P;j)h9KJ`722Ovd_K>FeUsDD& z{D-quYw99>6i)Gn^(4|W3*OT$aeCBF^(J&!%`%$l8mLo)dTs=&&Dflb+&S$!$hdu( zg)RDE9>{^z!|N9c4q1J_-N@7+Lm$FQ(-a-O78}I5s7p@{aj`t4<295uMao&GEH#-A>W4gvFan3ooY*y*^$Jw?a-% zG_B}7k=pPkCmL-e@Y@`djp{|>BkiIC29|crKtO(h`AFuH!a>OYb&z3ig=91h8k3N_ zr_%=+KP>EcL#6eGZ)lfJ2(0Xe+nQXf&hfQHPFn=!I=R*fnwWChnDScHYd7;%yc;m(S&fBEwkJfYH$4{b6`2){+O!(e57I)VZ2a&(`MzwM1p~S~S{jd#b70-u-jGt}mzq77!SEhE!zj_vi5=N7* z#1!y3N+jFg)Drx<#>{Z$5-X&d+&xAUXA_KZ)nvDPM6lhX?J>mA=<=b5S4eujN!*4k z{yyap0Z}~kj?ElahAcNyOXaPp)y-u#IC;H*PJ`#V^l2m9^eQPTlrlK$t_tO{X<>5k z(esI)qWnWTj`EUQ4zRz(U6ptjUmoT+^taz@8!kB0yhl_rBjkj({MZHYQlONT`?D^- z9e*pE+@*3b$NyH{QkUqN9~OZ*6Gb!$EeFyR&NvCjAd%`5L-3YK9oa?FbKm>**)?ld1K{*5+eSQb0u z+gz?t(`$2LfBiD1?l3Au4H#-+k%e!VHe&w=AKbD&#>7M?2Shzvs@` zk{jMsTn&l5v&~(S;8Et)0Y*_|3D%@$@{@!(`d(sBE}Xt(+#meTTc*KO{f=II`nEU`-B0CAw0?HA#@F3130)mDKiZL1NOX_% zPnwo76PIT_TrgAn&r40ci6NZc%vx>nI|jrhrUF4=%m)sb+?}uDU)>y0MKdPHvsNB2 zh)6|CqG1GSX@&MLOKaYDLM0X?xr^y21U_W1X#fBK literal 0 HcmV?d00001 diff --git a/pgame-plugin/src/speed-builders-schema.json b/pgame-plugin/src/speed-builders-schema.json index d56ceca..185ff6d 100644 --- a/pgame-plugin/src/speed-builders-schema.json +++ b/pgame-plugin/src/speed-builders-schema.json @@ -31,10 +31,39 @@ } }, "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" + "structures", + "scores" ], "additionalProperties": false } \ No newline at end of file From 5e8ee1291bc16260049f85a158676a482a7e6457 Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Sat, 21 Jun 2025 09:27:48 +0200 Subject: [PATCH 14/18] API: - revamped scoring, score is now reset for each minigame and stars are used instead - STATIC introduction, available in Minigame constructor - yaw and pitch control for start positions Plugin: - new minigame Gravjump in alpha (currently 2 maps are built, lots of features missing) - massive health shop updates: - shop split into paginated categories (combat, utility, potion, miscellaneous) - lots of new items - kits (8 permission-locked + 1 last used) - lots of bug fixes - use bundle name instead of QueueType enum - new xp calculation --- build.gradle.kts | 2 +- .../mester/network/partygames/api/Game.kt | 86 ++- .../network/partygames/api/GameRegistry.kt | 15 +- .../partygames/api/IntroductionTimer.kt | 80 ++- .../mester/network/partygames/api/Minigame.kt | 5 +- pgame-plugin/src/config-schema.json | 6 + pgame-plugin/src/health-shop-schema.json | 31 +- .../mester/network/partygames/Bootstrapper.kt | 54 +- .../network/partygames/DatabaseManager.kt | 54 ++ .../mester/network/partygames/PartyGames.kt | 23 +- .../network/partygames/PartyListener.kt | 75 +- .../partygames/game/GravjumpMinigame.kt | 319 +++++++++ .../partygames/game/HealthShopMinigame.kt | 182 +++-- .../mester/network/partygames/game/Queue.kt | 5 +- .../network/partygames/game/QueueManager.kt | 28 +- .../game/healthshop/HealthShopItem.kt | 62 +- .../game/healthshop/HealthShopKit.kt | 41 ++ .../game/healthshop/HealthShopPlayerData.kt | 57 ++ .../game/healthshop/HealthShopUI.kt | 397 ++++++++--- .../placeholder/PlayingPlaceholder.kt | 18 +- .../sidebar/GameSidebarComponent.kt | 20 +- .../sidebar/QueueSidebarComponent.kt | 2 +- .../network/partygames/util/InventoryUtil.kt | 19 - pgame-plugin/src/main/resources/config.yml | 13 + pgame-plugin/src/main/resources/gravjump.yml | 27 + .../src/main/resources/gravjump/castle.nbt | Bin 0 -> 21065 bytes .../src/main/resources/gravjump/plains.nbt | Bin 0 -> 20084 bytes .../src/main/resources/gravjump/start.nbt | Bin 0 -> 4769 bytes .../src/main/resources/health-shop.yml | 642 ++++++++++++++---- .../src/main/resources/paper-plugin.yml | 3 + 30 files changed, 1806 insertions(+), 460 deletions(-) create mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GravjumpMinigame.kt create mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopKit.kt create mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopPlayerData.kt delete mode 100644 pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt create mode 100644 pgame-plugin/src/main/resources/gravjump.yml create mode 100644 pgame-plugin/src/main/resources/gravjump/castle.nbt create mode 100644 pgame-plugin/src/main/resources/gravjump/plains.nbt create mode 100644 pgame-plugin/src/main/resources/gravjump/start.nbt diff --git a/build.gradle.kts b/build.gradle.kts index 54d74f9..e675c00 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ allprojects { mavenCentral() } } -val targetJavaVersion = 21 + subprojects { apply(plugin = "org.jetbrains.kotlin.jvm") } diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt index 62a7cc8..54b5e08 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt @@ -17,6 +17,7 @@ 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 @@ -24,7 +25,26 @@ import java.util.logging.Level 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( @@ -44,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, @@ -167,15 +187,26 @@ 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 = playerDatas .toList() - .sortedByDescending { it.second.score } - .take(n) + .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) @@ -189,12 +220,13 @@ 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) @@ -210,10 +242,15 @@ class Game( } /** - * 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 { @@ -246,7 +283,7 @@ class Game( } /** - * Begin the async process of loading the next minigame + * Begin the async process of loading the next minigame. */ private fun loadNextMinigame() { if (readyMinigames.isEmpty()) { @@ -334,6 +371,33 @@ 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( core, @@ -417,7 +481,7 @@ class Game( "" } appendLine( - "${color}${i + 1}. ${topPlayer?.player?.name ?: "Nobody"} - ${topPlayer?.data?.score ?: 0}", + "${color}${i + 1}. ${topPlayer?.player?.name ?: "Nobody"} - ${topPlayer?.data?.stars ?: 0}★", ) } 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 index 995a9f8..eed4c6b 100644 --- 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 @@ -10,7 +10,18 @@ import kotlin.reflect.KClass data class MinigameWorld( val name: String, val startPos: Vector, -) + val yaw: Float, + val pitch: Float, +) { + 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, @@ -72,7 +83,7 @@ class GameRegistry( fun getMinigame(name: String): RegisteredMinigame? = minigames.firstOrNull { it.name == name.uppercase() } - private fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name.uppercase() } + fun getBundle(name: String): MinigameBundle? = bundles.firstOrNull { it.name == name.uppercase() } fun getBundles(): List = bundles diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt index 1cdf272..48cd09f 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/IntroductionTimer.kt @@ -2,20 +2,33 @@ 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/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index eff23db..636be27 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -40,7 +40,10 @@ import kotlin.random.Random abstract class Minigame( protected val game: Game, minigameName: String, + val introductionType: IntroductionType = IntroductionType.CIRCLE, ) { + constructor(game: Game, minigameName: String) : this(game, minigameName, IntroductionType.CIRCLE) + private var _running = false private var countdownUUID = UUID.randomUUID() protected val plugin = PartyGamesCore.getInstance() @@ -63,7 +66,7 @@ abstract class Minigame( originalPlugin = minigameConfig.plugin worldIndex = Random.nextInt(0, minigameConfig.worlds.size) rootWorldName = minigameConfig.worlds[worldIndex].name - startPos = minigameConfig.worlds[worldIndex].startPos.toLocation(Bukkit.getWorld(rootWorldName)!!) + startPos = minigameConfig.worlds[worldIndex].toLocation(Bukkit.getWorld(rootWorldName)!!) } /** diff --git a/pgame-plugin/src/config-schema.json b/pgame-plugin/src/config-schema.json index 805e2ad..162a837 100644 --- a/pgame-plugin/src/config-schema.json +++ b/pgame-plugin/src/config-schema.json @@ -24,6 +24,12 @@ }, "z": { "type": "number" + }, + "yaw": { + "type": "number" + }, + "pitch": { + "type": "number" } }, "required": [ diff --git a/pgame-plugin/src/health-shop-schema.json b/pgame-plugin/src/health-shop-schema.json index 5061e16..8def21a 100644 --- a/pgame-plugin/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,24 @@ "arrow", "power", "punch", - "perk" + "perk", + "ender_pearl", + "tnt", + "fireball", + "offhand", + "regen_ii", + "splash_healing_ii", + "splash_healing" + ] + }, + "category": { + "type": "string", + "description": "The category of the item, used for pagination.", + "enum": [ + "combat", + "utility", + "potion", + "miscellaneous" ] }, "amount": { @@ -91,6 +108,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/Bootstrapper.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt index fd87a54..82f21e0 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt @@ -3,11 +3,7 @@ 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.HealthShopMinigame -import info.mester.network.partygames.game.MineguessrMinigame -import info.mester.network.partygames.game.QueueType -import info.mester.network.partygames.game.SnifferHuntMinigame -import info.mester.network.partygames.game.SpeedBuildersMinigame +import info.mester.network.partygames.game.GravjumpMinigame import io.papermc.paper.command.brigadier.Commands import io.papermc.paper.plugin.bootstrap.BootstrapContext import io.papermc.paper.plugin.bootstrap.PluginBootstrap @@ -41,10 +37,6 @@ class Bootstrapper : PluginBootstrap { .executes { ctx -> val sender = ctx.source.sender PartyGames.plugin.reload() - HealthShopMinigame.reload() - SpeedBuildersMinigame.reload() - SnifferHuntMinigame.reload() - MineguessrMinigame.reload() sender.sendMessage(Component.text("Reloaded the configuration!", NamedTextColor.GREEN)) Command.SINGLE_SUCCESS }, @@ -88,13 +80,23 @@ class Bootstrapper : PluginBootstrap { ) return@executes 1 } - val typeRaw = StringArgumentType.getString(ctx, "game").uppercase() - if (!QueueType.entries.any { it.name.uppercase() == typeRaw }) { - return@executes 1 - } - val type = QueueType.valueOf(typeRaw) + 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.type == type) { + if (currentQueue != null && currentQueue.bundle == bundle) { sender.sendMessage( Component.text( "You are already in a queue for this game!", @@ -103,11 +105,31 @@ class Bootstrapper : PluginBootstrap { ) return@executes 1 } - PartyGames.plugin.queueManager.joinQueue(type, listOf(sender)) + PartyGames.plugin.queueManager.joinQueue(bundle, listOf(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(), + ) } } 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 index 08ff1d3..c59d515 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/DatabaseManager.kt @@ -1,5 +1,7 @@ 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 @@ -53,6 +55,16 @@ class DatabaseManager( ); """.trimIndent(), ) + statement.executeUpdate( + """ + CREATE TABLE IF NOT EXISTS healthshop_kits ( + uuid CHAR(32), + "index" INTEGER, + items TEXT, + PRIMARY KEY (uuid, "index") + ) + """.trimIndent(), + ) } } @@ -210,4 +222,46 @@ class DatabaseManager( 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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt index 34f9154..4f84078 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt @@ -2,7 +2,12 @@ package info.mester.network.partygames 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.placeholder.LevelPlaceholder import info.mester.network.partygames.placeholder.PlayingPlaceholder @@ -17,6 +22,7 @@ import org.bukkit.GameRule import org.bukkit.World 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 @@ -103,6 +109,12 @@ class PartyGames : JavaPlugin() { fun reload() { reloadConfig() registerMinigames() + // reload minigame configs + HealthShopMinigame.reload() + SpeedBuildersMinigame.reload() + SnifferHuntMinigame.reload() + MineguessrMinigame.reload() + GravjumpMinigame.reload() } override fun onEnable() { @@ -112,6 +124,7 @@ class PartyGames : JavaPlugin() { 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 { @@ -127,7 +140,7 @@ class PartyGames : JavaPlugin() { queueManager = QueueManager(this) sidebarManager = SidebarManager(this) // register placeholders - playingPlaceholder = PlayingPlaceholder() + playingPlaceholder = PlayingPlaceholder(this) playingPlaceholder.register() LevelPlaceholder(levelManager).register() StatisticsPlaceholder(databaseManager).register() @@ -152,7 +165,9 @@ class PartyGames : JavaPlugin() { val x = entry["x"] as Double val y = entry["y"] as Double val z = entry["z"] as Double - MinigameWorld(world, org.bukkit.util.Vector(x, y, z)) + val yaw = entry["yaw"] as? Double ?: 0.0 + val pitch = entry["pitch"] as? Double ?: 0.0 + MinigameWorld(world, Vector(x, y, z), yaw.toFloat(), pitch.toFloat()) } else { null } @@ -178,6 +193,8 @@ class PartyGames : JavaPlugin() { override fun onDisable() { // Plugin shutdown logic - 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 index 2a6e76f..2003d6c 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt @@ -28,6 +28,7 @@ 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, @@ -64,7 +65,12 @@ class PartyListener( 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.lowercase() == "ez") { + 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" @@ -142,21 +148,52 @@ class PartyListener( @EventHandler fun onGameEnded(event: GameEndedEvent) { - val timeElapsed = (Bukkit.getCurrentTick() - event.game.startTime) * 0.05 - // increase the xp of players - for ((player, data) in event.topList) { - // XP is points gained + 15 per half a minute - val xpFromPlayTime = (timeElapsed.toInt() / 30) * 15 - val xpFromScore = data.score.coerceAtLeast(0) - plugin.databaseManager.addPointsGained(player.uniqueId, event.game.bundle.name, xpFromScore) - plugin.databaseManager.addTimePlayed(player.uniqueId, event.game.bundle.name, timeElapsed.toInt()) + /** + * 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 = ((xpFromScore + xpFromPlayTime) * boosterMultiplier).toInt() + 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 { @@ -183,17 +220,14 @@ class PartyListener( 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("" + "■".repeat(previousFilledSquares)) + append("" + "■".repeat(additionalSquares)) + append("" + "■".repeat(maxSquares - filledSquares)) appendLine("] ${newLevel.xpToNextLevel}") - appendLine("+${data.score} XP (Points Gained)") + + appendLine("+$xpFromStars XP (Stars Earned)") + appendLine("+$xpFromScore XP (Points Gained)") + appendLine("+$xpFromPlacement XP (Placement)") appendLine("+$xpFromPlayTime XP (Time Played)") for (booster in boosters) { append( @@ -205,6 +239,7 @@ class PartyListener( } onlinePlayer.sendMessage(mm.deserialize(levelUpMessage)) } + // increase games won stat plugin.databaseManager.addGameWon( event.topList 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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index ccda14b..b37aff8 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -5,12 +5,14 @@ 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 @@ -28,10 +30,11 @@ 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 @@ -59,6 +62,7 @@ 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 { @@ -67,6 +71,18 @@ 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) @@ -76,11 +92,13 @@ class HealthShopMinigame( ) : Minigame(game, "healthshop") { 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 plugin = PartyGames.plugin + fun getShopItems(): List = shopItems.toList() + init { reload() } @@ -89,6 +107,7 @@ class HealthShopMinigame( 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...") + shopItems.clear() config.getConfigurationSection("items")?.getKeys(false)?.forEach { key -> try { val shopItem = HealthShopItem.loadFromConfig(config.getConfigurationSection("items.$key")!!, key) @@ -113,7 +132,13 @@ class HealthShopMinigame( 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 } @@ -145,13 +170,15 @@ class HealthShopMinigame( * 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, @@ -174,13 +201,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) @@ -221,7 +248,7 @@ class HealthShopMinigame( return } val survivedTicks = Bukkit.getCurrentTick() - fightStartedTime - val survivedSeconds = survivedTicks * 20 + val survivedSeconds = survivedTicks / 20 // for every 20th second the player has survived, give them a point // 1 point every 10th second if the player is still alive (last player standing, time is up) val survivedPoints = floor((survivedSeconds / 20).toDouble()).toInt() * (if (didSurvive) 2 else 1) @@ -260,7 +287,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 @@ -268,21 +295,13 @@ class HealthShopMinigame( setMaxHealth(player, startingHealth) player.health = startingHealth player.sendHealthUpdate() - // reset perks - resetPerks(player) } - // start a 30-second countdown for the shop state - startCountdown(30 * 20) { + // start a countdown for the shop state + startCountdown(45 * 20) { startFight() } } - private fun resetPerks(player: Player) { - player.persistentDataContainer.remove(NamespacedKey(plugin, "steal_perk")) - player.persistentDataContainer.remove(NamespacedKey(plugin, "heal_perk")) - player.persistentDataContainer.remove(NamespacedKey(plugin, "double_jump")) - } - private fun startFight() { if (state == HealthShopMinigameState.FIGHT) { return @@ -304,9 +323,9 @@ class HealthShopMinigame( player.sendHealthUpdate() // time to give the items! :) player.inventory.clear() - shopUIs[player.uniqueId]!!.giveItems() + shops[player.uniqueId]!!.giveItems() // 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)) } @@ -323,7 +342,6 @@ class HealthShopMinigame( override fun finish() { for (player in game.onlinePlayers) { - resetPerks(player) // give every alive player survive points if (player.gameMode == GameMode.SURVIVAL) { giveSurvivePoints(player, true) @@ -459,13 +477,20 @@ class HealthShopMinigame( event.player.sendMessage(Component.text("Left click to reopen the shop.", NamedTextColor.AQUA)) } + @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 } @@ -529,11 +554,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 -> @@ -542,11 +563,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() } @@ -570,14 +587,17 @@ class HealthShopMinigame( 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) { @@ -589,11 +609,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 @@ -624,6 +644,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.isIncendiary = false + fireball.yield = 4f + fireball.velocity = + event.player.location.direction + .multiply(0.8) + + // 2.5 seconds cooldown + event.player.setCooldown(Material.FIRE_CHARGE, 50) + } } } @@ -635,33 +671,33 @@ 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(plugin, "double_jump"), - PersistentDataType.BOOLEAN, - ) == true && + if (getPlayerData(player).doubleJump && player.gameMode != GameMode.CREATIVE && - state == HealthShopMinigameState.FIGHT + 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(), + ) } } override fun handleBlockPlace(event: BlockPlaceEvent) { - if (event.block.type == Material.OAK_PLANKS) { + val block = event.block + if (block.type == Material.OAK_PLANKS) { // start a nice animation that eventually breaks the block Bukkit.getScheduler().runTaskTimer( plugin, @@ -675,7 +711,7 @@ class HealthShopMinigame( } remainingTime -= 1 if (remainingTime <= 0) { - event.block.type = Material.AIR + block.type = Material.AIR t.cancel() return } else { @@ -689,6 +725,15 @@ class HealthShopMinigame( 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) { @@ -705,6 +750,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) { @@ -752,20 +802,16 @@ class HealthShopMinigame( if (player.gameMode == GameMode.CREATIVE) { return } - if (!player.persistentDataContainer.has( - NamespacedKey(plugin, "double_jump"), - PersistentDataType.BOOLEAN, - ) - ) { + 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 diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Queue.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/Queue.kt index 340245c..53b13c1 100644 --- a/pgame-plugin/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,7 +94,7 @@ private class CountdownTask( } class Queue( - val type: QueueType, + val bundle: MinigameBundle, val maxPlayers: Int, private val manager: QueueManager, ) { 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 index 9b34179..f4b5df6 100644 --- 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 @@ -1,22 +1,12 @@ package info.mester.network.partygames.game 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.entity.Player import java.util.UUID -enum class QueueType( - val displayName: String, -) { - HEALTHSHOP("Health Shop"), - SPEEDBUILDERS("Speed Builders"), - GARDENING("Gardening"), - FAMILYNIGHT("Family Night"), - DAMAGEDEALER("Damage Dealer"), - MINEGUESSR("Mineguessr"), -} - private val mm = MiniMessage.miniMessage() class QueueManager( @@ -27,24 +17,24 @@ class QueueManager( private val queues = mutableMapOf() private fun createQueue( - type: QueueType, + bundle: MinigameBundle, maxPlayers: Int = 8, ): Queue { - val queue = Queue(type, maxPlayers, this) + val queue = Queue(bundle, maxPlayers, this) queues[queue.id] = queue return queue } private fun getQueueForPlayers( - type: QueueType, + 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.type == type && it.maxPlayers - it.playerCount >= players.size } + val queue = queues.values.firstOrNull { it.bundle == bundle && it.maxPlayers - it.playerCount >= players.size } if (queue != null) { return queue } - return createQueue(type) + return createQueue(bundle) } fun removeQueue(id: UUID) { @@ -52,7 +42,7 @@ class QueueManager( } fun joinQueue( - type: QueueType, + bundle: MinigameBundle, players: List, ) { // check if there is a player that is already in a game @@ -69,7 +59,7 @@ class QueueManager( for (player in players) { removePlayerFromQueue(player) } - val queue = getQueueForPlayers(type, players) + val queue = getQueueForPlayers(bundle, players) queue.addPlayers(players) } @@ -82,6 +72,6 @@ class QueueManager( fun startGame(queue: Queue) { queues.remove(queue.id) val players = queue.getPlayers() - gameRegistry.startGame(players, queue.type.name) + gameRegistry.startGame(players, queue.bundle.name) } } 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 index b56f0e2..af512bf 100644 --- 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 @@ -1,25 +1,54 @@ package info.mester.network.partygames.game.healthshop -import info.mester.network.partygames.util.createBasicItem +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.ENDER_PEARL), +} + class HealthShopItem( val item: ItemStack, val price: Int, val slot: Int, val key: String, - val category: 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")!!) + 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") + @@ -29,17 +58,32 @@ class HealthShopItem( ) ).toTypedArray() val item = - createBasicItem(material ?: Material.BARRIER, section.getString("name") ?: "Unknown", amount, *lore) + 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 (key == "splash_healing_i" || key == "splash_healing_ii") { + if (group == "splash_healing" || group == "splash_healing_ii") { HealthShopUI.setHealthPotion(item, key == "splash_healing_ii") } // apply regeneration potion to item - if (key == "regen_potion") { + if (group == "regen_ii") { + HealthShopUI.setRegen2Potion(item) + } + if (key == "regen_v") { HealthShopUI.setRegenPotion(item, false) } // apply speed potion to item @@ -56,8 +100,12 @@ class HealthShopItem( section.getInt("price"), section.getInt("slot"), key, - section.getString("category") ?: "none", + 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..c86d355 --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopKit.kt @@ -0,0 +1,41 @@ +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(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..2df7b48 --- /dev/null +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/healthshop/HealthShopPlayerData.kt @@ -0,0 +1,57 @@ +package info.mester.network.partygames.game.healthshop + +import info.mester.network.partygames.PartyGames +import org.bukkit.NamespacedKey +import org.bukkit.persistence.PersistentDataAdapterContext +import org.bukkit.persistence.PersistentDataContainer +import org.bukkit.persistence.PersistentDataType + +data class HealthShopPlayerData( + var maxArrows: Int = 0, + var stealPerk: Boolean = false, + var healPerk: Boolean = false, + var doubleJump: Boolean = false, +) + +class HealthShopPlayerDataType : PersistentDataType { + companion object { + private val plugin = PartyGames.plugin + + private val MAX_ARROWS_KEY = NamespacedKey(plugin, "max_arrows") + private val STEAL_PERK_KEY = NamespacedKey(plugin, "steal_perk") + private val HEAL_PERK_KEY = NamespacedKey(plugin, "heal_perk") + private val DOUBLE_JUMP_KEY = NamespacedKey(plugin, "double_jump") + } + + override fun getPrimitiveType(): Class = PersistentDataContainer::class.java + + override fun getComplexType(): Class = HealthShopPlayerData::class.java + + override fun toPrimitive( + complex: HealthShopPlayerData, + context: PersistentDataAdapterContext, + ): PersistentDataContainer { + val container = context.newPersistentDataContainer() + container.set(MAX_ARROWS_KEY, PersistentDataType.INTEGER, complex.maxArrows) + container.set(STEAL_PERK_KEY, PersistentDataType.BOOLEAN, complex.stealPerk) + container.set(HEAL_PERK_KEY, PersistentDataType.BOOLEAN, complex.healPerk) + container.set(DOUBLE_JUMP_KEY, PersistentDataType.BOOLEAN, complex.doubleJump) + return container + } + + override fun fromPrimitive( + primitive: PersistentDataContainer, + context: PersistentDataAdapterContext, + ): HealthShopPlayerData { + val maxArrows = primitive.get(MAX_ARROWS_KEY, PersistentDataType.INTEGER) ?: 0 + val stealPerk = primitive.get(STEAL_PERK_KEY, PersistentDataType.BOOLEAN) ?: false + val healPerk = primitive.get(HEAL_PERK_KEY, PersistentDataType.BOOLEAN) ?: false + val doubleJump = primitive.get(DOUBLE_JUMP_KEY, PersistentDataType.BOOLEAN) ?: false + return HealthShopPlayerData( + maxArrows = maxArrows, + stealPerk = stealPerk, + healPerk = healPerk, + doubleJump = doubleJump, + ) + } +} 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 index ddfd204..9de18c5 100644 --- 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 @@ -1,8 +1,13 @@ 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 @@ -23,36 +28,30 @@ 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 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 player = Bukkit.getPlayer(playerUUID)!! + private val databaseManager = PartyGames.plugin.databaseManager + private val kits = databaseManager.getHealthShopKits(playerUUID).toMutableList() - companion object { - private val maxArrows: MutableMap = mutableMapOf() - - fun maxArrows(uuid: UUID): Int = maxArrows[uuid] ?: 0 + /** + * Get the player + */ + private val player get() = Bukkit.getPlayer(playerUUID)!! + val playerData = HealthShopPlayerData() - 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) - } - } + 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, @@ -60,13 +59,16 @@ class HealthShopUI( color: Color, potionName: String?, ) { - item.editMeta { meta -> - val potionMeta = meta as PotionMeta - potionMeta.addCustomEffect(potionEffect, true) - potionMeta.color = color + item.editMeta(PotionMeta::class.java) { meta -> + applyGenericItemMeta(meta) + + meta.addCustomEffect(potionEffect, true) + meta.color = color + val duration = potionEffect.duration / 20 val minutes = duration / 60 val seconds = String.format("%02d", duration % 60) + if (potionName != null) { val name = MiniMessage @@ -76,7 +78,23 @@ class HealthShopUI( ) meta.displayName(name) } + } + } + + 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 } } @@ -121,29 +139,82 @@ class HealthShopUI( } init { - // init maxarrows to 0 - maxArrows[playerUUID] = 0 - for (item in items) { - inventory.setItem(item.slot, item.item) - } + renderInventory() } override fun getInventory(): Inventory = inventory fun onInventoryClick(event: InventoryClickEvent) { + val player = event.whoClicked val slot = event.slot - val index = items.indexOfFirst { it.slot == slot } - if (index == -1) { + + // 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 } - val shopItem = items[index] - val item = inventory.getItem(slot)!! + + // 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, item) + removeItem(shopItem) } else { try { - addItem(shopItem, item) + addItem(shopItem) } catch (e: ShopFailedException) { val message = when (e.message) { @@ -151,7 +222,7 @@ class HealthShopUI( "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( + player.sendMessage( Component.text( message, NamedTextColor.RED, @@ -170,40 +241,112 @@ class HealthShopUI( ) } - private fun removeItem( - shopItem: HealthShopItem, - inventoryItem: ItemStack, - ) { - purchasedItems.remove(shopItem) - money += shopItem.price + 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)) + } + } + }, + ) + } - 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, + // 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() }}", ) - meta.displayName(meta.displayName()!!.decorations(decorations)) + 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.category == "arrow" } + val arrowItem = purchasedItems.firstOrNull { it.group == "arrow" } if (arrowItem != null) { - removeItem(arrowItem, inventory.getItem(arrowItem.slot)!!) + removeItem(arrowItem) } } player.health = money } - private fun addItem( - shopItem: HealthShopItem, - inventoryItem: ItemStack, - ) { - val sameCategory = purchasedItems.filter { it.category != "none" && it.category == shopItem.category } + 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 @@ -211,7 +354,7 @@ class HealthShopUI( throw ShopFailedException("no_health") } // check if we're trying to buy an arrow - if (shopItem.category == "arrow") { + if (shopItem.group == "arrow") { // check if we have a bow if (!purchasedItems.any { it.key == "bow" }) { throw ShopFailedException("no_bow") @@ -225,19 +368,9 @@ class HealthShopUI( 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)) - } + sameCategory.forEach { removeItem(it) } + + renderInventory() player.health = money } @@ -299,6 +432,12 @@ class HealthShopUI( 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) } @@ -331,7 +470,7 @@ class HealthShopUI( } kotlin .runCatching { - purchasedItems.first { it.category == "sword" }.item.type + purchasedItems.first { it.group == "sword" }.item.type }.onSuccess { material -> addSword(material) }.onFailure { @@ -355,23 +494,28 @@ class HealthShopUI( player.inventory.setItem(EquipmentSlot.OFF_HAND, shield) } // process golden apples - purchasedItems.filter { it.category == "gap" }.forEach { item -> + purchasedItems.filter { it.group == "gap" }.forEach { item -> val apple = ItemStack.of(Material.GOLDEN_APPLE, item.amount) + @Suppress("UnstableApiUsage") 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) - } + // 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) } // process regeneration potion - purchasedItems.firstOrNull { it.key == "regen_potion" }?.let { shopItem -> + 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) - for (i in 1..shopItem.amount) { + repeat(shopItem.amount) { player.inventory.addItem(potion) } } @@ -379,7 +523,7 @@ class HealthShopUI( purchasedItems.firstOrNull { it.key == "speed_potion" }?.let { shopItem -> val potion = ItemStack.of(Material.POTION) setSpeedPotion(potion) - for (i in 1..shopItem.amount) { + repeat(shopItem.amount) { player.inventory.addItem(potion) } } @@ -387,20 +531,20 @@ class HealthShopUI( purchasedItems.firstOrNull { it.key == "jump_potion" }?.let { shopItem -> val potion = ItemStack.of(Material.POTION) setJumpPotion(potion) - for (i in 1..shopItem.amount) { + repeat(shopItem.amount) { player.inventory.addItem(potion) } } // process healing potions - for (purchasedPotion in purchasedItems.filter { it.key == "splash_healing_i" || it.key == "splash_healing_ii" }) { + 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_ii") + setHealthPotion(potion, purchasedPotion.key == "splash_healing") player.inventory.addItem(potion) } // process armor kotlin .runCatching { - purchasedItems.first { it.category == "armor" } + purchasedItems.first { it.group == "armor" } }.onSuccess { shopItem -> when (shopItem.key) { "chainmail_armor" -> addArmor(player, ArmorType.CHAINMAIL) @@ -433,15 +577,14 @@ class HealthShopUI( } } player.inventory.addItem(bow) - // an arrow is included with the bow - maxArrows[player.uniqueId] = 1 } // process arrows kotlin .runCatching { - purchasedItems.first { it.category == "arrow" } + purchasedItems.first { it.group == "arrow" } }.onSuccess { shopItem -> - maxArrows[player.uniqueId] = maxArrows[player.uniqueId]!! + shopItem.amount + // 1 free arrow is included with the bow (which you need to buy an arrow) + playerData.maxArrows = shopItem.amount + 1 } // process tracker if (purchasedItems.any { it.key == "tracker" }) { @@ -453,27 +596,15 @@ class HealthShopUI( } // process steal perk if (purchasedItems.any { it.key == "steal_perk" }) { - player.persistentDataContainer.set( - NamespacedKey(PartyGames.plugin, "steal_perk"), - PersistentDataType.BOOLEAN, - true, - ) + playerData.stealPerk = true } // process heal perk if (purchasedItems.any { it.key == "heal_perk" }) { - player.persistentDataContainer.set( - NamespacedKey(PartyGames.plugin, "heal_perk"), - PersistentDataType.BOOLEAN, - true, - ) + playerData.healPerk = true } // process double jump if (purchasedItems.any { it.key == "double_jump" }) { - player.persistentDataContainer.set( - NamespacedKey(PartyGames.plugin, "double_jump"), - PersistentDataType.BOOLEAN, - true, - ) + playerData.doubleJump = true } // process flint and steel if (purchasedItems.any { it.key == "flint_and_steel" }) { @@ -484,5 +615,63 @@ class HealthShopUI( 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) + } + + // 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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/PlayingPlaceholder.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/PlayingPlaceholder.kt index 80ced46..11f1023 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/PlayingPlaceholder.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/placeholder/PlayingPlaceholder.kt @@ -1,17 +1,25 @@ package info.mester.network.partygames.placeholder -import info.mester.network.partygames.game.QueueType +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 queueTypes = QueueType.entries.map { it.name } - for (gameType in queueTypes) { - addPlaying(gameType, 0) + val bundles = + PartyGamesCore + .getInstance() + .gameRegistry + .getBundles() + .filter { it.plugin == plugin } + for (bundle in bundles) { + addPlaying(bundle.name, 0) } } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt index 7540fb1..992756e 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt @@ -24,12 +24,17 @@ class GameSidebarComponent( val data = topList[i] val player = data.player val playerData = data.data - 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 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)) } } @@ -53,7 +58,8 @@ class GameSidebarComponent( val minigame = game.runningMinigame!! drawable.drawLine(mm.deserialize("Playing: ").append(minigame.name)) 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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/QueueSidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/QueueSidebarComponent.kt index e86f761..26ec10b 100644 --- a/pgame-plugin/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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/util/InventoryUtil.kt deleted file mode 100644 index 1d0b9ea..0000000 --- a/pgame-plugin/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/pgame-plugin/src/main/resources/config.yml b/pgame-plugin/src/main/resources/config.yml index 8c59465..60f0dda 100644 --- a/pgame-plugin/src/main/resources/config.yml +++ b/pgame-plugin/src/main/resources/config.yml @@ -9,6 +9,10 @@ minigames: 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 speedbuilders: @@ -51,6 +55,15 @@ minigames: 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 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 0000000000000000000000000000000000000000..255014fa47dc365434f4f8a398fcb518cee6a770 GIT binary patch literal 21065 zcmeHvdt8*&)_>(uPh!wH0s~&4P94ldKv5Cpa{BpdFGUcV!YFbPVNy`T5s-Uk$q6XW zkwTCG3Oz!JAdaGPRT0#?Y=4uv^$X0{!f{E=AJJLzlc5je#){X z-~8p9*DOn;u7AyY?a#(({f(j(->$n@A$}aCX!W=H&u0`}9*5SRy!mKE79G9J@Uyk) zqa#NSjrG`%tTNm=s;Ja4A8R-n((73oX*qk|Nbl|UNAH^Njvj6pl->Nf$J69klS0Sb zzG(P*l#S0rS@YoB2lvwLEvA=My>U5S%Ws`U?MKOaRYhL-Hqg>b1b$i?yUqlQIKYT1^Yb4oBriNpjvQ3}iUtbk4O+B48 zWWbBX3rThe_gNFfpCIg$!c`O{Q7`bK^N{l%rlRtmm2qNIajNdRO-guEPWYk82%q=5 zO8DE8623qseEp<^Pf-cqI4R-h>MMu3;?AU`$yZ5}KPhPnIcaPsV+V3hm1C1pW#D^N zN(+5+Qqo*2CnLsi2R@z@`*kYz^C!hVlVd+AD-PVKVlS8!dnw0$bW+m0sMzx+#r_D# zeo|%}xIx9<2malQPT)8LDaGL9_6S zV=)faVM!W)#`{-zCZkxT2fTadfkVxcE_i}hyDRS7?RVF(qV43J8`gKdE86Vu{J7au zVlhxMeR}(F*g-yT@XX*}P8XInDyBqiHC5~$*`Fsb?VoPaKfU7Nul2?TmDi)Un!d8z z_T#+xBKGl;E_(dwX4~3~G`@=@I}3$B*jQ(?a7P+%nI!J0x2cYQ-xQxvH<^y(eVusU z_fFYcB+fb4@KXG}wWng}(&)-@qbqKSwPAns8G1|ha{Rr&bbDANu09)n*=_mvqMX>H zu4}4g8J$5ceFa&9Ep_4r_nUP4FS$8==izCQxc^LHnOo=e9&I1%mkN3Dz7g^si!RwT zYUR@6k>`nTvleTovzYo1M1BMN*O_@T{BS)Aa1gP|v!E_lRctDDE$} zx9IY)$T!|K_)%W!&O)B!(a@y5<_66>^TRJUmgJ4_pYc&tpv%ox`JWf&PW&pF%lVP5eYXtlemsnt6mqy|!i2Mz>HF=?GwG4MU4&7b4vY2|gR zm?$9TyPl~&;qL^aLH?Q0gAO4vdGlVT+TD#dp1ftCc3qbGTXg$d^obzFN7Xv<-SZpK zfbk_XP%_0$ z=j-)n$A)cOV=K!@3v!WYjA}q^*zz^DF&fmIk+#!ClAk5mTqj<5ze%rO9vc?C##WJ$ z7O5OU;d^yza<3C|w60Fb(WW{fM;q&e9C_3UIdZ8Ja>TC_aiUt`Ou+f}XZ0kyiNSy^hj zkR|2lc$So-vsqG(va_Td6=g{|%E*#(bS6v6(YIMrjta7*9G%FLa+IGX{eIK9FjnOoTqNIS30Rf$`m^K0Se3uy zA~}&IU{r2xdY_{v{r-TsFjnR8MmbpmR^_~Yd0ZH)@?#lk9AO%Al#r3e5#}LB;)xCj zK8PqzgD?|0I+>Bikt`#Pqm+y^j$jZR6=tMyq{v9)h_|}mrmRuoAQ#1Sh@8i&ewqt2 zzmXuE7By{2f?e71B0b0NHeYTGzZ|($Ui_n9`tFnQBLUaKR+z!#+c)PKU1?O5^P|1i zMSIBw@6@L|+%z^&@Dutm9ySYhr0FknMig0>p1G#FHlvg8(wChTy8I#7T7OLLG5t_e ztF*k;TQ2np4lxKXU7=Vx&8L?qK!{53;_L9%=y=NoVfCx&6@p8#r>OtMiYV>vw(j~= zWc4N;?{z3*dk?{4Kb)2)%4Tjo_nnFNHyICyUMk{5Sn$5?z51MUH`J2^kU96vI* zIqZ)K8suXie%`1j4vLVpAtnx_h1ai6sv|N`MDFkZC5tMRT!OlKdj;ro`#X)The)Je zld4tmk!=O7S`|^(yb-b51CweKGCD!fl_rTZIvMgaP{jMCEKYhfjKZyM(KT@t2q;nx zL>(Ad`AOd3oU@RlHzhlUQQhD>q`LP11Jy;Q<&x^&^s_B8g(#)3h*4151_jE6yGiVD zTiKlrHz9Lz8Va_(a+5;Q>a&HOJd*k4AFzr>4DCgDw<<68l85|rqXd%!?tQ0=PJE=t z@!Jub>mpp0G}9;D=Y3M2aOO^JZ+S>|eVHNyjjY;8qu7u^E%@IgIe7U-B?kBJU{-rH z)`Y%RWB8ffShjsCq8u+@s2wY;SS^i@N!}j#`AFK$v}FqzT9meorG;tB=7iOwZcg?6 zWSZlG>NvroSQW(yAn>kiieB8@M)bnmhRq83TSz~*Il|pniEog&{?Av|yK@YNJUkhwuth2J!lQ@N)#2x(e<>H=LL93pm zU}0L2CnPTM%n$2{3-g$HyKkmXd-xC5I_1{sqx|dE76ZkZsX_!Qvl>I5)bSwVRLPzP zrv?QzdA$p}`L!KH;tYFnhbYv=GCtD&eaR+W73oVhv7{&Y$f3=(TwJfB<>m1^V};sr zMENvp!xx6v=j>Bb$v%C@Jq6}zLGRhfvi8-xJjlM=!M|26{ya{Qsw-IuEw8hYo!zG& zAL+bJvJzd`Xd}zsS0DNyyRU;UEf<@`3NF9u$-_mQV2i$_z*MD{4Nyx5zoA^56DMfZ zg*toRkdrcY;TGk<$<;;%F(MyiD?0dtO8V6b zfjj9Wd1k8RFC*pB!PEyrya=y9yN@XQXG!)7tbL$M&qlF+xsnpg#Vl>4!KLB_`;>Gd zDes-Y8gGX3FJLl9drN-MQxT1PueqH6CUg)hn5U<*mJ6_!#Xj@Pt>F|gQU`2F4g3dW zGf+bf)Wwq&yoSYTS`v0JuU9J@4x;1|8HcQd(Fbo$ zw1+OO4{3=wy89t4BXU1u8AdAwUefM9#qwcmh;Z9xK~1v0)G-W2B-|nlj9ta77a|)~pg0XhbZW8Z;sT?0|(UlY|8WwIA30)cTgySBFt}4gc zGOokJJeEcuwDy=JI;y$7tYJ^hC14FmG_RxM4|rvGLK?d9-VZ%J+dkA6zUhETheZ;_Ie{KNy7DFO^9G zZyG1nF@zlxo)k9R_`Wy|@z)a@rtm{cQlwa`Fsl?aFt#{l1MRF7$!Q2*596`t32(Ou zUj&W^u^NyIzC~`MMBo06oY)$;@!C zTdlcqVL5SOyJEw~r!1XjF=@V?5rl1Up2Q;zz1lhSo7@)0oi;FpC=876)uOtGt}H6+8J1rvqIo15}VgF!M^dhkNuI;%=)9;`fi8FA+YB73c)6k<)WHoJ!w^}u(RB{uS2vZuqI=LfG-l8*6{VD z*|EakG?o}dJVV0R!fLwy}0P#i-r zJkUvoWE~<<3PZ3tP$5H#4iSh~ZmsF=JbUH6_UTCnJ~HH09rs#iqQ$te#2J1r7L_0I z^&E%8S5%$w($@0ZY%vhBDdl(b;ueXS`N}NSl!_APJk>Qa>hZ=nQXlCEe{=75za9P!0RKGBps!1#tTKmP)pR*vQ?dE^?75qs!R5nRSS$xD|sk7o^a z7=-5(}BC37ozfs>nfm)Dx)41L|$(Llw_t~vt7vKG_#wQe_l$WiXS9CxnY2iO5 z6(y>qDILo+N{?#xnQYA0nbT3;$YM=F8l~S)EPU@nb>Y;%jW0Znb}%XfVmO~s7q)dC zU-p31q9xB-bn<%kaCaf)Q0u-2?riQBF^ZWNpI&Z{a;EXFqD zW9Y%PZEEGM()zeL$O%;Ou#THjZu%9fR;cNL&|vPBl5@rYff55#`j^Uu8 zdM+;U*JPC^37*gggK)`ouD)r^`x-(UC}zIPy;8AmSF=9zFHP1@(Vx8dUG=yWomLsv zgv2>}Zisp5Ih$+EbjmIbLDa*c)}GyzD=L*4RQ69Wh^K3u9W>1Ue>Y<^y;a}s0LKzP ztWrZ2N{@YFs^!9@(95PX{P-3pKjQOPXFVtBz?B76u1=X{j(cV#L}0JPtwrx*UAn!~ zn_a2iYV~HQ^wIU?y&*{;qzjO2VJKa@HF9Ou@rJ361*;q%YA1N`kZ?C}yl?3^ymDHC z`=wWT7B|YXlU64!jj^~f)p5_W%4bul2vJ|r&+G+*U_zwGxWHXBWL#+2`m;xS#0X!z zRc2_?>d$Y{GATCmx+QzgBywcGgYCyeV&+J+|3woJUam{`zSW?gR3}i8H0UOG&0H*% zXbRLNtMI#CZpjLW6NuxZmz2b}HTBcOkq1%r>x_6LA+~3#>s5|nsayqlELW4gXn2WH zg-52UIk^^eqEy9kWUj`Mv0mgs2aYUtG!jGm+#fN$WBUk%H1}U-=#IlpWr`?iDNZbK z=s}c;t(S7}9$z{>wBNx~$NN_NWf~H;d6Ax(*I})spPn98A=+> zX{r%a#C0fjX0e?I<0|ojo{r}RS!~z#K5U+JHg0Ny=nD1@H@a5e(KWID#KFn?@cN{m zo_@J^>DGqt44aeZ#;rH_J*>s_!_4zpmC&hG5o}V;b9^`z)37n>1V*DY(@5vyzJqxq zj>2bs6v>_35E(S{mhmj|&3EL+nG39a6d9^l0amx&FEx&Sd-21(kqtr`IrWW?;$-J@ z6$7r-QyqtAFI)J;D`+ooo-0~fl`|kknIRk?T{x;~83xT4sac_at`>W#vS7xV;=7+u zLFujh3m-aAfmzE@_U!( zF>WxuVr~-L17=XM$5+b(k{Bz+@f#}M084vf8BJWq0;pZOR0-B-mk)N40Z_%L{2+7T zqiE!`HQ4Na>3DB{@F%D?LL3x3AqPE0kJLAfJ?FutbvKQ|9agB3515;QNGm9FD)YEv zpbV;IEVD7~A}ZJl!?#>gU9y8=RzTteQQiAA7`Ih5RP)hC(MQjvq$>$^EZqu{*zYDS zYTN`wx6$|&f!j!^a-4DF5S?jjKH-IOZ0-}Y+r;c7m<71DvwiHtjVaHIf#}Rnz~GZS z_Yc8V&OyX}+(zt&KQ;A4LC8`0Qj*dv@=Ool+ndwFCjG^kvagt`DY zn~I0p=%*3}3O>XFPVIgw&gkJKfyqeI3qDOVpsyhbm4l znHGr)lmAZB7e%TV;L#OLU>qt##9kOv#i6k-dOGzfP=2IOl;{5G#UDK@zf95^JvaXH zLwcnFq)^wbW?u5ho7D`ra`^-M)Cq;y+d(HQUf;C>hPeiZ@nzj?Ml&rF7cSuewioHkdR@CNe`8l2QU4xV~(F|k%maDipWBt(We1f z9{Q&QyO2wo)`FR$DY8l{8g0TWiWS?iM3^v~nDvv^#M9WuN1D+*;uOGKskAU&>rd&T zqUd>;8)I_>m=P1kLVY`lE~>hgcERBKZ)g#kZIo#!k|Kv%c8T0VQ<3awJ#7Mzvs8<% zy6U6pqXr?S-JgZS@2-Gf`qcV2VcutUy=j|rw!O~GPCaIJ#;%8;j^#{zy?FQ=;&9_V zk&(vSFClG~cpWx>nyoO;#v6>5~TgU)89(!|E$l*T;|D z|CGJh1TOvu=dz!0Sa}T^Q*AOzn=m}TJx2JnYT;rv&=gyosq+a@VKiYi^Nm-{>;tfN{`(F zoF6Cu5g#qxn@3-Ag`93c(4Ci!5D{|PuFS3NGCrFLoetw>fIm%U8SiUIuYKwDw9Clq z4?QQthhSP%-Td@uRa|&ALyKa=CsJJaB8KphR{?#RRn^^d5?h3P!7dzj%-aJt-BFvP zX}t)xh3)hO*hoGOzH1&+ z!gzO3N?h3$`rBMp)>xC@Z&(q03SYzJ`|rfb!xn#;B>zGKOF0)BCQ!+Rh6&^nn0u_l z<4v5T2e{T_ubF>$2c56ElB5?lLDFlt@rl8xDb}*@;mExE1P_Zh)$*)dl5aw9<6?VvNA?kw{A{2)ZCmBNyT z*KiiyMflvJIaC~qmHu+-g_w_Cq*tRCiU8_`Ky25pH(0Q+C&;x$2IWNt<|;Zo2Fr_k z;=)^bgJBSKO<*o$_X?1FSMvGg#@Lwx@irEkVJ}=kx+b3|fQaTb*fe^GhH#oSV>uqK!b5CY zf6}TLkX7;GyBm_B<6KL;@ucFVD2}=}@TQC2)Y6+wyph|+RO6u{nW-E692w~!jzX)? zFGW(P7fHU7t7yONNF&1_U-mw@HQYqwImd=RasY$kI4~#<$z$#UVrub9I}>uvb3#z> z-9j5ZULCvnSib`|nYe*vVF@v4Mq98DJcNU7_N1+5DV2zxxTif{D!$xklnth~ZeSXI z+BxC{`Q@YN$AM+&$8>)tq&I&_Hw$q?v%wS6KM(EQz(57>s%91ig@vTb5bAIbYKN!O zwzHS^q9g#fOATmmgouh)v4diY(60LPvk-XRc9dUr3(LtiX75TxDBcVx=Qfzg&?Rd0 zxr*RYcCR<8{ftj|CmJ58qK1=GAhxBIr^SHBcb!@$tkyDHp*5#MbTBoTw01rKo#@3w z<(O4arO!|)f6i{CDcbYA<&i1XFuukV%1BfM%ZYU(mC8dY40G{$F}OuW^-*@g&FqpU zG={IDuI(1EYsbXPSU_?x;pONRyyRf~%j2cghlU#$J5QXLk#F++)S)EwAM>u;zd<#S>(T4QE;Q&fBou z<3L_~0=A8p>{@M#FWS}wl%3+M1}rP5_&OQSK-Am6{$mh2JBsjSStDgZbkAQj%_;37GS;f=R+w^VX zPrxL#Djt$Y`#rz`{YLQl_dvt$n5YDjE`g-e1gJg%`lmF36!APC4(lnvY^)d2&R|jB zxdpJYU6m2Y&&60IcGHLu+<@8(;e-KQ-ca6|E$GB}K;ka%@exfR<-P*{$^blW<=;tR zDqs}0jT8o;uFOrI0B{6zlNl?z)v(&2@+j4Q5{S_9y%nZUU9$%s69JLyDBgliJVe#W;cx+fJRH6 z0)_v#02!Y#f*b>+(reVK-vJoIwdC3u%JJN#9M99C>-|TeI+BNhZ)2tpUHBJfWJJq4 zB%17YEuh&19*fGUa$?7$uRfq6^PQ%{nlQZrmEgCbrBZdX*PR%MkUEE$YRF||VFhx4 zX{(B31_rW*89~t>a8zHjn-lw*%fG``;W(&yF;{y%nD(Lg2+;&!`7@177B+p}!eqpT zR=_Sa0g-cP1sibeuo!UzoYKPX;t*CMYCz!JUR=zF>cHg#t6MJ4ie^&}3{y!RdRNfvPTZSLzoNuaa;=lqX8Xfc_4kc@fF* z{{t-N$k#A`YifQQK3|(YSY|3X|7S!V@n+h>Hl3nuv3z3(nDa3 z&f>^%5Rmujpwb=5wR{SQ``FTOxWTN~n3;Jz3C!^j0C@`vOJ-UF|8)_?@ zf%8MR6Vzh@oFpc8QI7%hDU0%m#!0h2bc$0DO~C45AGAwA=aZ(nG|s;TH1-V7-vGvX zqRAdOSQ1Hd8NjUB($PD|CxKF%wPd7#9CBS<7_Bbk6Yh;tj0Dhu=0$sq6d-I1?6M`D zwaYp0LtX-sZG=>%hDX@DLtf@!iTXB3O=tqD4pM)|K`+fN1*vg7&`NL$c0_7`VDQWU zW*upd_yg%UfNfp0R|6|MlXWK9nGQS!DC@kO-?MuWiFIC|q}Ys|edWk0W!Aa$9QD3^ z6PtBrBv+~qzcdl0_J4rmd`uSnph97@ic}~^UL!G5Y~+Nqa<_qMq-L6|kYw?_D(|U9 z?kVw4_{7$2ZB@zI%WiGoAx#pxkg{2)=*JpOf`K3Q=SgPcHyHROWY`LGRJL9~O!3PN zi^Sb$?1kQ@?b|SV0DT;JR$7tD&1e_^%mVdovaYu~{rPY=^22Zil0U)aC%(Oe_4`MV zWbxGiN}^3UGkVEQ`#NN?eo6U*&QIuhVcuu}j^EvSD)8#!91?Sv-2b_FUvJ9CeUgWK zoPGr|Glu$j)hzQH@&wj$10S80Xo(L$Z-msXsbR#p;AUyDe3njs@{78 zRlEFG;FlwZ@s043+4bqi@DWSm&C`%$Z5eWh{Aq=XKzu-6xdgwAG_CX^|Mrrbj<@M5 zFI$@hr0r*+ut=kSM4mc;Gfzx_i2af6fAC44z70csz{|;*9Mu~Z0CNkNJ_GaUyaOO0 zooeIYsd?x;vSm3Xl(y zgQrK4z`D4M$r96Fs)9t8v>wT`np2op_w4FYq`cH-tfK7_u7HzA4mcIN3ulO5@9|AP=ZzMoH;O)w5n&LtPa~$%EwXi| zQA~38H1dp*Mlsa*HsQ(zc;R%0y_w;Yehro7eJkL{*dkl}g)0}KNX|JF!OgfL-7zVU zR>vw+j12;JV=<6C4^GuiT!wlj1*nJf{*~sc8%aNmCx37kr0Q+O!?r?{C=L-8V8sz} zZ||}y_DSnLv{m?k-2tg?_VVFPR5xfsK}~#K3INl@ucCzX0OjYW zvQQg@Wx$?i$?X%^t)>!!zvHE1=AXcXKTTKwr62*M37RD?J}+eAZOg}KSn}dx%!N{# z5DeU;D?l?;;=7deSo3!jn@jp;Ae*kTug%JAkI*@@5N*^`C#3Nt9m%N>krX)cBM49J zq4|b$1{MbRuaeAhkQvT{1tUGoOf4TJ&;b}4^`N6d2~+RE;>>FlUVo()2t1;`_a*F?mnUCpNUaL zEO=n~Q~WyVMY*ohxM~z+lLlnTwKCez_M^^t(2Uwl=vG40-$>-!^tD6`#c#lF8kN;c z(L??s67nR3Y!sX${;rtvfZ9T8WFaDO0ZFfinW(4)`d#AVZ%`hq$x$DC>bCAZ+rmH)ZH&NKl+geh-3 z>Q)_t%U^P+UXX-u6B7l__`?KnldZMk|AJ^s>PZ zLfIVc|6qL;0_;s%Nh~9F^;N?&6FLnkEru@vwo$9EP^-*(fq>5aOX&oc1q$lTXHaoh zC57vg^KBn@_68K+77tq`YePYR6Sx==}x>U;?wHQ_G zCySaw7Nw19_z@Fw_^={^lCBp-oGh;}S-Q``E|54j*fpX3w?TDOzVCt(H(xvp-!0q%E=nBPJqdl-N#eH%S zlDFZ8RneAC&q5tY9%*&gaT2Z%H+JSX&=MC#`@XDgweSi56_K}lizECTR7q=s!|<2g zebfahy1??;^X$ONR0=;lFI5rW=_toQq{ppUnF{!lshw;}Z9hb2{+56#0l~{Kt>-$6 za^SfFRgZo6Iv5TH(vQ$0R%feI1LM7Lq3oCH12zhr6u}~5ty^bWZ-HZRCa=xtY>6p! z#Q+pG+NR}W%@c;L_~{WmfSF2*Tudejo5*7?>D;>+aj__#>w6&uM}jWte=@B1uA2S9 zaP{CzOuz`CNXE!8mqwTYR%i3iG<22FKKd^djhLQrz>E$)Vn1}y5PKRZg@9F?^#T-U zfUUE;|7w>NzLGS9D%-A_ge(0^b=N!i2s*MG-&7svdX@{O*mz?A zDUp@Aon0r?a!s8Pp(zH~rrK&nZ8Uv)x2u}Ew-&7sU^EBOjx*glr>|($woJ5-?AZIG z@Dp0Mu#Zvd+Y3ijH(hZrXTn36S|rZr`UE+nc_5x556<=cB{J)hE;2iXYd;j1AnawA$@aJZI#=#|d|4Z5(;Nz0P8! zXZDHqEoF}j{ga|YUa0KN@g8bjmR(wSu)p22tgB&uz*z8=a>cMh@k_PM+_90!6?5$F826Sng$&-2_x6@qEpv|u zd4Y#EZih5{(BHbO1-DkXZN;r?{ps0@q9TVXPwE(ZL=Jx!c5&`IW7jH6OJ>_fyc|7T zDI3g<3hnRH>9sG4?7JUh++IA|<=KAd8A<7f(NTjBM-;|UkMDImj};i58j}{DFD(fh zywiVju=3EFl2C=B^fSNcu%SwY>#=KN-Q`Edx_@kHHXj>1lG|=O+MuwnA9&cGvfC(s zXvAlA+sMNK{3yD=uia?qLG;kWv63kJmA?*}>x`{7j|v^?ljOEV3acJFn@5L@bV`i< zqZQ3Dr8A8b_fDr=PI>%`qS7;JsI#%FH@7X?;GOn}kJA0^~mDd&5CEgg06{pF{z-k^wI_c^*=>prqFDpE0A9dl^)CR)l=^mpG%Ott&#lu}?vh%yw)~2_Qj`~?|iu|?m+QQ<{{?U>5AKvND9g_>p zMj!Q@A2g0q$n4wtBMr51;5l&xlV) zgu^RG9^5lF4jYU)G1~7lf9#}L-*o<=`J<02-#jLXGTh-*thjsRYzVY7_SFxbo~1)J z>+K^(r@R_<^Ks5BJLiVG&yPKfDJ&lC^R)BmI@0bjVksHwTpMk0t$pm_aGCwO@9g(n hZU5Hov&7uChFMxaUyhR~yvY)}gMfj$0{zK9+Patmug+;y<7Do7I0@ zbY{b|2L*!XMbYm6e)q?*lfQiZ$y1%hy3(rV(V|OVbsRW*!?b;{I@F@{h;Yd{E3u5W z2*-58+R*Zx>lSkkFMBvNd(^m1IHr;0oYh%$rtJgXLZgOf>922_Go#d^(QhBC2Gs2a z%X^lT-7P|kX4}GnAs1hzYg6M#0kMk`rgdeWiCIu^@zIp34JSBF%^%goF4{4zEBj2) zlTDTV`wKTN;UB)Xb1=)(w`KFWm6bDno5PwGKjU-Xhx3gGU0ATByUxAPQ77S=JXSoqWN?_JYp27rg8kL{7e7$8gcP7f#qQ9Ch*qI|i+D zFKo4AIN;<9b_~~@d!f&cq0Y${>=;Izd%@j~A;-xV>=+uHd!fX`w1_olWqtp9u)!%U zkK7=s`z&kxr-x`V+g$HWdh6HXdOyPb)C=a?AYZet?TNlo0eGcn*C6A^YM%AGT(5IYm4 zPBM{fXTnKMXzffGoMU2}or!8^m=G51POw}U?HXMg*HO6dW|Q%wny5wj)4CGR#CVr% z5)3AY67mX5g5w2)1)6Jdhiwo0=j%J;Z#V}+tz9$3oJW^#||-mg&P$7x6VX4HH28MS=zHc_f< zVXM(_^H<7Qy3J>b7YzQeX^mKYtusiuN34FL>w_eB>8Fa{dmAT*amxexN0-; z&cM~c|xHLu|a*StDD}YOIY?TREP)UailQXv2l|il>|YuBeRD z&Pi_`yspZSl&LoVIKM0H%ufpoM&#-GqE!hYvBky@lDb-0zSkEA#G8zXO?usx0*(5p zPmB57%Au!y+ahwgEj`%^7g=e6CY$fYQD4#~h>iTDE|I*@+ciNJRcyR_wXk%9J~~#J zZ7>U)W4s~hY_VF9RHBVjr+-wlt~kLID6I5PY0^u#H)%d9EHQU&zjdwDU#Abekv0%h z-Jr-66c-NGE<35xB*jOovy-|CG&AB3r^|T7>UWb$a^&gmuIlutVs%ndNrRl5Cz6(U z4}M-VcTga^s+IF41qUAWHK)6!EG;(5JDa>Y3E4INx{LBco|j@YIjJj>MIeip+2lR< z{L0XyxrMTjhLt>R`vS#iT2fcF$hRI6xzpLS4I5PZ>*_!~e{Pm#EJs?hL2s^WHGUhV zzR4HEj6+~qe~(h@_+ll@3>x^3(SU^Pb93ghlMsIBv6R7v{$^#iRTJ_+<(}iZI^WVc z>(x(y$qymj5Vlk2&2>&v_@Xx$h(p{Id@33jEn{(!rwq8{7uQ)9z2$8kE1;ubW*Wb zl~Ys^2|DNM1U_*-DpBfTY5RgfLCR){XMv_l&~&FWsHLz2f+LYMC`Qk5ThiMXo~{Q| z{qeo#&KmyVQqlRCg@Z*39drX*S>ls0EnaZKrJ+RoM3-P7M$mTD7F1YELSPB4+jVJE zYVb(8JS8h|Feak+kZM%uuY5X}tJBhsaSbag-F(e`L-}zDV{ON>p$Khf6OWTHCg^R7 zSk$6csp6D|I!R84tJ1I~scRcUnMtM@9fFBn8d?>tzQz~tlLvZ=Qo|uluhszg;>jrT zUL#)&{yCylFG$m?wFcf>6{UvX8Tewj-qMEFfV){O9>dFJed*9>oxEzR3bUo+ciTP(`!hsP+g46m}}%l zcyZy07l!^Etp<{%h+zwufOdxsXbYKuW=24RDZw6gNVbHy!z{ui6_#%m5Xv)~#7eTI z!g{P;*z{v)W^g87+$dLlB9gFi4Xmm1rX72_OqKo6teX%^vbU)t;{~gdrB}7_NnMO- z-6{8l-ZL{cOXf*js{W+_x(x>h1r_kW3b=NW#J;vRqJh-XYCOGtrms{n znz>oh5vRvnefyar9RSGCvwp{bS!)@R4?#j~Y-R|1vDSqoYN)*VYh zaD=bHI^b(+#@Av#sg(t#jEv^O{V3EE2iunVvINU|+GlvwQ=x>im`0<#0%aZYw$8rnWkHVaGznC}QRi0$3=r*WeFBt4u$R zAPgV|cpy7Q{}j=5tFwhKQHrD?-h-yoSG5=U5^s@Iwt$hHmM@XuVc}`v`eQn6!>m@2 z_YR`VEq~vvzM%EJso!1IzE`1YuAtSl4Z<7 zMM`RKP{blr_UZ%~x7a91>e9;7Pe5AhicvMFn)#CJaxPAzKY0z-0^1IRqS;yf-{^kw zR;1MXE6trjLlE8qh3;Gb2$b$Fc!1bWUDYO4sC06!l4JG6$Xr?Imm&_tfTVOAi6BpG z{0#ApJC~s?BTzgwlc!j~MCb5$%OgSG3LdYB5VFbWtJ1=$P$WC)OgoXtRR)F4?>-2Fv5YcZ#&{+ z{Z;K*zQhF<2BSK4b$)zNX&xRH4x0XcRm<8Q|M0TWNw!DP<8Wxb3>v+2HJuWzYP!t; zFATlDbSDZ{X}D{KygTDLvHHEFk{0>g5(@7@kxlw^!O5#ykZ^8_Cf9GMG`16wzrw&kxY~=2$4TA1tetNfQ zaU6`f#2XoCQXxP0e0PzY2}jLVeM*s%;&tDmc)?u5`xPn`A`K1#(ZJkX!8Wg&7fu^xop0E#vE27PIru3tZ<-oW8x|^L%d4$2&a%$&^22_K;N~zxI zW$t|yht@2v^{_%`*OtnLZuLUJAV-8EZ|4aP>eA12-3iMAQ<|=PRKx8(R60`=Bmvo< zrRsGaS-S;PnRGV!v}jAolf967xt7zh#cGtoNC%%4b|En|_3sfIpNKb|-dVl##1M9_ z_t%ZYC1mrx&BxvXjO^sOBaJ_#hj=L<0I}tizpeqR9~EYM_ucHQ30YX8ZRu<(#U7tY z^}dV6p0K-ujWa^M6}tB+kL%1AQSP!RGA-ZjJRuROO|||y6^ml?YT=SB6tw=MgGC09 z)|wzi(E3H5)4D!5Q@pT1_bAlr#bKCRv~(JK#?vHp`IyorOk4KBovH~dv&~QPrtS?- z>>B5eWqkf{`&a*VipKLjn+FP-&l~yFxSp~*XY0gWesALvFIT6@FRTCf)ykxrf+@Oq z_sAyeKZ|xBmH*53xs8i%>8}?*qY^nsJb1j+;r-v&POWRQz5g$|^kKQ}39nZs)j7=o zaid@M$En(u6BuUUw>=zuMy}KFv&~suMY|gwLZJPBySzDw_YeZ|{(a>D-{%3>9FyfN za0zKXfAL`mTG_JsbhPsXjfFqid|vx72;47iKK&3TF{TJ>K7aio5M+Jm_e}poIL*+X zTQ{F}UIo-m^P(#UHaf3j)5uxBXG$Li!E0+P2i|uc0dCZ~ z`ycSS2wv~MyRi8D%J8@s%6%rRI8N(6aeT*d#bGOHlrvjhbZV<;XSUKhwbeRj zwz}@rR!+%k#F?$KoRU?8Q(MWlLyr&8iGAAJb!rVWdeqYNf*y)(k{a7tJ=oe@^HQ^Ha@C9LkEjl zWgaev_msrZ5aBvB$G0t|=>w%tOK`(V<{{Vgc3TP!x{d(fM9D5#$IjntjQ`jO*nr)DE%%uOC#cwN z2fYs*7b2Rpb`+ge3)Tk?`MBP3$>K#*~HcyG#^zD?WL#9Tp#Q7sEsu zaHggVQGJ4fCT^p@GBx3X8xPPkvrX32B9+DZKfnwaopmgnQrZ1ev+Xi9+X&E8vE4E? z+v!uXZT@abb!Jb^cKy_B=UDoM2KPPXK=GLFKcM*yC7{)<=?_e@JaQ%ojQJImZ#QiQ zjs4_gJ;88BX*q;B1ZOmqz!|^#gDiZ;;HIPtQ}SN+C~~ zD~M=Iu^bt?96XfA{9HNoQ)uENsY;*3MXAb!$2(GTYu^mc$aM?E5dB8*i1Ccly)1jR z(!DZ0&G2#kn8fr-R`cnopAauzp`d|*Vo9ROI^?7Ov!0u1UKH!_QBM4lX<#DQC#f*a3;Ei8#ScSOEVG`nhN^31Z3@S!>EaAMWa z(`6V^bh!RJvK%FqZ3u2S;)hsiF$>Me|ZZ+X6-@{Q2jilelpwKp-~=Y*I%IZ_pn~H z0PFj(p7`CZ`C;PyeF66!?tFoT`wl-yEJxGc;+63)dx88Pp9lGMzXkdEx8Sv3fY;bB z!0U=+Q+$%rr{^>1h8ajoomLdi$&yD#%rDTM|rqWf&)jnH62Gu@(PV4q;Xo|6z@zlUde0>!`lTl(G&q0ft+AzT15BmOc{^V%1m0B_6%(D=ZxOfCYA8;3YHJcx(l52!W$L6sRXSLO$VW z2Es{>LvR|(9pgSja736-Vk>?%7Zr28foUCi`zCN0zY2VfCE?dl9_9>5$pXqA%tLA# zrWM3E1Hh%Ipl#IL!Ty3^Fy2DDny~AtQfz4pAk~ush#hV&8LDa@3{?P!B9!DTG|FxLO#~u_*KL%adt3i#+_lUu-;f=T#%b9#U z+hM{d@kzY6`ygn0ZGQ{pS8A40oDukBu zmk@duQ&9KBAn;xSFArQbJ~U^d+N;@Fy?tic&}xcu1&DHw{Q*&KLyuBSkH|rP2=5?0 zo&k321lGg9M-+UkX82pYnzj~~R$XflmS;E&NEb_1WA1Ro_)vNg;T`AT!~uQHoHEx4 zY}DB(SASv+@~#oQr)dU86$I`@r1XJX`io>`wh0PddDcNlq08h?>4yfv;I{Hx~zOk*xN z%seO$@4J=l!Fl}h-bPI$s0w@;WG+X(m;}c-uNCw@a3vJp?%i@6PCYeh`P8Uu;1&-? z&7B&xYHHNYQ=_hXFzWt!s6LQa=OaA9H)p~({rK?6u$8dgIj~qaA6V>UAj9hb8l#LU z1dz!aKo-iV^|$87-&|qNhaj!N%ne}B4Mj4OHxy%ep7m7w#C5y}v{kX#@#G2_cLC_2 zq|HoF(x&?8Bq(hADpMLWY)jpkSoKw=oanSBZ9&z~v&tN@IFRjo9>)3|MX0ZeWn4J8 ztQ}ySA=&^;J@xL5r<*{(U(V2Z&#xR4G*d}+#nie0)fW|(LKXRj)AWd zDD0thnHZ_~=ihYS4wqHHH_$3yzi8%pFf_e&& z6j9k<&rrmAhOAE-aaujLNT#} zf_eggs&+y%@2r8Ke}`b!ev!g9Hyd+l2yh77;fT2tkCy$niHSuBk<$+syU(aMXIHv4 zO#=lVr~i@`ypg4+cRk89DK2l|{tp$>)BLkeLWBrxsI4iAuLPeT(_z)R50jZ4;@v%P z037BQdMfDPoIn!1y&iLiyw=b|D$DN z#~3*^adbj{0C7yFWHL!4gl~Eqj>e<>G5+Pw(}yhcKH)-=U}AuGY7Bsg&xzJ${5 zzn23LrRMts{Y!+bXHTGjlD!oz}W6)id(I4j;YB%-3VwLE60 z?!7yU4iFp?!gIH^hJT=-e{+0daab(%KSZ$G)V-*%p%}*rw=yqkh`a$I)xL>OiJ*4J ziQU*2dE+^s#i(gCxg^iQ6!KeBXUh%G{+e`Z)-(J8SeC zp!VpC2=N_=m&gAGYX3x1od8t31VA|eLU}YF4$T<&HK==*ODX2u z3ixo&&L_auQ{I%8JD&$*ubuucHZ4!Cu4WKt&wkIQ<<}vS91_XBa7Y`S|VUP)ee}@YpZ9hzbX#` zBBybv{(LU1trVamEvnktsX|1`KI8&@NERe~7(oyxWY|Lpgx3OlKp|{Rq8dt~lYp)b zueH~~j`cEo+Gg$&(2yO+!w@7020^@+I71)^2%bZw4T3bb^EZ6R-4(QJK|2E~DVL=B z^cavcq$SXdYF`UATnqxxZmr&}rMmSZ2b_>K?uX%DLM2W)$lhyq`fUrIdmvOy;<{$}%oq%rsN%1id zHnEKghnt`!s~A9e05T-ut&ni!8TS;+`t%OH383sCR{z2EO9l#z^U-nXt~ia^FSA5g;eADp6& z+V$W%YP{H*4Ai_A;+R;G*??e7^U@wg!aH{YsdV?%Sy1-Ox{s(riA4Me>-~2KcvtKh zP31jROJoz|Y|h8VmMi2ADqi?*5!{T0oocr;j`t?N`$)!oul zpKA-%d3UIp5eo}DNp)TtvY#B3N>r@r8dg3ifj7{c6R*>XmovY5+#DI zE1=XZP*v?hsHz&>9F%{Zi8veBK#zVL%XHP(lji4>zU@Qw?^qK#gbk|#U?N|=josD) zF#31!)D_g$`37vZ?FO6ss5i700tg2XOd|2GXxra>b^3m+eTX%`)mVFsHuya@aC@=l z6c7ESN2tF9$gt`ALHUP3$0l{T3AEgQ7cMutHke$S5gmA6~Dcrp_u5A$tohuoU+O>~Ogozr5&?2EF^ctnYUG zmgDmbr{@56V_pY#wA`XVIosp=5UK*L%vp8%e(26Rf!gP7=UHfueA5popk4oXS zP-{3Xy@jQ~m!N)6a@MbbKuki~_l2kck_mhXit;2yeNBM_V(FGjnHvyR{KFSW{D4_O zqWgv#Su`BTc!!<9OAARxhkj6p0}yCdkd=LNOOGp~2@{I}2;vTr&HXXiv=K(=IDr-` zQM;BH*|#D*l#a;NfHk}WM%rmS`K=ZNOPXAkA9#%*`Po{Sye=l6{Ef}cRTdXtW zLoIi{fLb27vN;B3jWDsoN`XEb&P3H&2aI|j?x$2#p1HkCw*J~!x~#&SbG(Wv)WVT7$p!P;*!a`X80uHvcp7H zHVJH8A>^tNZ{mY%2+C6XAuTM0s}9}5Z{`G7@eDHAytGHkl@u*@fdls3pUk?BjRDG~ z3=gfKQ=h#ae&kNYNm-*@^AD;|U{`rmbzIUL7TJ&e)Poh!v6tGg5G_KHAGB0Ss%N?Z z45qJU+Q_50Lp8;wgqF`Y^A(6j2f-_|U9a*lOT0LQ_ z?%+1o9<3)VRW%%I{4@ppWd1hF&fci*_;QVet`d6;TM=}m?UG!96rUZ@Bxz+Uj0S&Q zRUE?1(W%F-IqQW5a{!AV%e38iQfGODBrb%+aW@gp0V`&3UK_CLGh4-qOT- zi#F<=F=yhR)`X;Ml$8uWa?_@;$?#8IPMY zSCxL8Pn{#%JqEQGpZ%V`IAU)xp7^qbl1J6&*!LYe@BR+^M!$i5k5a2|)@?oAN*t#P zk3SLqd*P(`)XN5XAGmE~_V;c%4oAn;Nn6_Y(`QeKn!e|n0=373QFmv#-`3OTEK~GY z_`v<(9!nPW;vdjs>HiaX=Pa}d=Kc+V^tJtLf%`hbcg~9l-%nv>_d%@quq$q_VI>YL ziBHmv4s%yOJ~X3HOMJwj#8zRZr*Ix({>*OSw8zEl@w<_g?!GFe5`J`g<3_^_v7UDAyVd& z)jSvSUAySegfCqlK%a5038luC^`i1T*hDI^-cmBjgDxsfufQ=U!I1;oK-mjTE1W}M zxK}SdDT_$+OfqI|hz(TCRmm9QBdWJHI=|SX7)4X6xu{9C3X2P|$o8pBMZcNwy@zz! zRqJ;j_KYy8T}7Y!*`wun=CHw0&k3KB-1{8~b;%ra07^yMHSlGv1}vdxB_ml*O0f2P z%ArdB(Wu_f)q z;eK~Aj<&O~B3TBs*|NihsZsp0hTzk*TuPUnj@fsjF?mwTGa<(@ibMTDG(-nE%3!*< zZxPDN67y#K0dCKa_=_jh)fg`5d)4^2J=QG8Uuj`LqW0t0;xEd`U-J9#Pc|SN*HKKa z)2rddUf7fUT1fBbOWiEyL&tj?TFj~N6p5TY7^28tMLnxV`c~(V!(c`?=m*$J)rwmD zMIG+AV}HO9@B{NPdf7W~4yDwc!s@H|^D@7XTIL^UlxAN5F?_`*CYW;8r#l#-Am_VDzgM&}B4u>%VJ_v9P!|>@Fxcp~^ z6R+bh$M7v#?XTOEB78J|y*DRo}k_$WGlQ{o_ zoPPQ{-IK50Gw9}&z3{462z@j58A$Y-S$lSN)srSS_p+~E!7eXfkT=k$4o@P$KlTdV zwGG1?lX83#pP~;Vo>~l(d7RoI{};Q={fkIR{djglrf9$nDxpWP!4SH9-gJ5JZvc)Z z;Auc&!&<%{j$O2fh~|@eVjMP$22#iX4j?u zs~6wM&2Hcdx#zlf^x_VwKF)HWMSJ#kz2)b8525jP1J6=tHe9*UTR-}9hcM{0v0pf* z&9j(vE|xp_mOJI8!eMP`_Ndx2tSPUbV^T>hV-m}4b?MkmiDh^j&us1()2KbhN~}Evwff=dsr1}F+^C>Iicn}-q@X=_3zwVXSvxiHhQstMzRGs j&Xx+tlm||m1|;>PM%5}8i}t4F(c?G%q;ws8!4TdGNI+|7 zYp%N6iUEoSKp>Ij(i$MER0t&jGz%1hP)G@n7_#SF>tG@#{IP#*=G@$K zzVCM)-{XAWcS#pqS7ASMtx&=F;9C!8yz;#J)L%dR>`+1F?Qduw3NtUipH(QA_KNm-F#m=EHIS6uclr4mZ;9>q)CsT6{`^OD2b_ zn>&uCyVkf(RDew~x%9`L_raLa@nloGcZgpRV+0OhkNlG3EqJzipWs>V%O|;!m8^`{ zIk9soAM7Xe)%51bRpon``}=B`U<4lJejK&9kf=za^7}09CAejLE3zxDTBKlwM#xLW zXZ@H?N%d>GGd5R_?vCtwzgl$jbw)sBS7xXN1;Rp=r*$`$)V@69$%dB`0igza}?AOvXqm99~e(U`2vip3sM(>k@vnUjp8-YF#Z$&izSD22-=N+rvY z-KCPXYLydb+<}@^NNkWH%t3S?m%SW0h|4BPC3v;Ug)@$$8VZR<8FFS2U5*?)hFeO>bD&^o(|OYg~TEm@;bSPBN*_lI^Ma*L4+^yW}yYw9O6C5Hw&U%+IO%f9O8-O zhJs#~kRkNx`7}`)PL#%;ntreIC`V?q{ZvRKUxCe}__aTyZne!ewJ_Uy?ZLW|2gsA5 zzg+)HU0}0;OU>F18+!G~ixYuEy{WO+{JdI8jMul=9$=g08$tK^u<4wyAoKV7MY#tW z@a1Xqpc`BG=pK*DmxLwu)8B51tT-@9x*l7}D4oT3lDl_x7+q4vBRY^nBOF;|MF|m* zhMfhOpVqBr3r|>)9%fIxiM52TD))bV<`Uc1`b#Lrp#6=Q+1g`UpseY8!Ica6vaYxl z${MWz7cVSP9OnaN?YUh7Wlf4|hqBi0;Q(3J5p1j*1wGO(1bwn$`)L%6Ru!V1Z46Soc3e@z*F3eRcv*h7{I-ACxTA-pHKvCM6 zVdxI>4xq2Wc;FM<8lW_36z~=GOU<$KPKR~c^B&B)A;N|wE5GUskD|5NnBAsmnBC@t znBAUZKI)gB09|`)GiGjN05dl_iI~1#&P^>hI81^Hvvy@m61rgp7{UrLo9tr__e=$C zNvLG(NX4NB)n|gW;GeRO(8unRvXXg!(jIfvj-5-LkxLg?(PzlwAVLI5kmQQ`Icm3_ zO$WrWU&vShagCDL;ak!ThFB;a;bZcG!z}mo zLGdU>uSpu3ruaIyZz3xSqMX_TSd`U}Hb!BOQ2qdp62mBd3ThnK*-Guc>p{Dls0Nu! zElOqcCFx}-;vxF}h+OTY2Y3)58uyhE&wxt`-{dR~nUeug~Gp6e)8Sm)* z|BjKYUjVR8d=+4${sH)ySY6UoHhnA9%+K^Q8H z>881(IV1OfHWFCJpa(+FTACbF!%A(Aoz)H05;pt-!w6P@39*X-=K3P86!ZGLofqYi zlWHaqR25?i8+JhrV=&U(gpuY(;{PzxoO}^!exp4r(f#pc!-AhXDh|$!9S=CV)Dh)$ zoQGT*&-DGLM6#ju_Gb7M1v(%$tL87k;6VqMNoWR61?sM>ZM#8|j=2ibbuN=aWsPU{ zf_$wf0b(Z)By5@)q;60|fla`yXD0y(3o3^ZnDLW7#I|HK$L4Q^XG#wFb?be1deGSmIlP_1oyLI z{9mAo_*@86!tIT)%*c8XVQmy1u+9JCA z4&^aC1y$OE7)Kg{_isK?9gm2JiZIG-OzR@t0!hVtkz1~0A*c| z$@&ib{I_2xyV%EIslZOX?E_PRJq%0*HZcz(s1(gu*Hp`BUJ$dtD(&tB%Jby;XY!s8 zEow)fX{Xxs1Ld7~Qg}-Kse3%5p3ydG{&B%b3Y+vI^)Ei3zS(?laqOzr*wv<=>M-Kl z%-8!H{cm`YE~St%o*vfDM2K9o-}8!3Qx<=<9BNedcELa{;5;u#h=7w z`GNVTau#FFx(ZGpeR1Kraw_AVY-FS%imse&sT#Qv(`20c@znf8+7V5PA#iT??#}5e zO{RDE4VbdR|5j1M#;0p*M!S{fT=j9Ur;35WiRPV| MRUf_kWYwzw0uuEnr2qf` literal 0 HcmV?d00001 diff --git a/pgame-plugin/src/main/resources/health-shop.yml b/pgame-plugin/src/main/resources/health-shop.yml index b358963..a2ec187 100644 --- a/pgame-plugin/src/main/resources/health-shop.yml +++ b/pgame-plugin/src/main/resources/health-shop.yml @@ -1,4 +1,5 @@ items: + # Combat Items stone_sword: id: stone_sword name: Stone Sword @@ -7,7 +8,8 @@ items: - 9 out of 10 cavemen recommend it. price: 8 slot: 0 - category: sword + group: sword + category: combat iron_sword: id: iron_sword name: Iron Sword @@ -16,7 +18,8 @@ items: - 10 out of 10 blacksmiths recommend it. price: 12 slot: 1 - category: sword + group: sword + category: combat diamond_sword: id: diamond_sword name: Diamond Sword @@ -25,7 +28,8 @@ items: - A favorite among treasure hunters. price: 16 slot: 2 - category: sword + group: sword + category: combat netherite_sword: id: netherite_sword name: Netherite Sword @@ -34,7 +38,9 @@ items: - Only the bravest wield this mighty blade. price: 20 slot: 3 - category: sword + group: sword + category: combat + shield: id: shield name: Shield @@ -43,6 +49,9 @@ items: - Has a 1.5 second cooldown after use. price: 3 slot: 4 + group: offhand + category: combat + knockback_stick: id: stick name: Knockback Stick @@ -51,6 +60,8 @@ items: - Great if you want to be annoying. price: 5 slot: 5 + category: combat + fire_aspect: id: enchanted_book name: Fire Aspect @@ -58,6 +69,8 @@ items: - Let the world burn! price: 10 slot: 6 + category: combat + sharpness_3: id: enchanted_book name: Sharpness III @@ -66,8 +79,8 @@ items: - May cause a bit of pain. price: 12 slot: 7 - category: sharpness - amount: 1 + group: sharpness + category: combat sharpness_5: id: enchanted_book name: Sharpness V @@ -76,91 +89,9 @@ items: - 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 + group: sharpness + category: combat + chainmail_armor: id: chainmail_chestplate name: Chainmail Armor @@ -168,8 +99,9 @@ items: - A basic chainmail armor. - Good for blocking attacks. price: 8 - slot: 18 - category: armor + slot: 9 + group: armor + category: combat iron_armor: id: iron_chestplate name: Iron Armor @@ -177,8 +109,9 @@ items: - A basic iron armor. - Good for blocking attacks. price: 12 - slot: 19 - category: armor + slot: 10 + group: armor + category: combat diamond_armor: id: diamond_chestplate name: Diamond Armor @@ -186,8 +119,9 @@ items: - A basic diamond armor. - Good for blocking attacks. price: 24 - slot: 20 - category: armor + slot: 11 + group: armor + category: combat netherite_armor: id: netherite_chestplate name: Netherite Armor @@ -195,8 +129,10 @@ items: - The strongest armor in existence, imbued with ancient power. - Only the bravest wield this mighty armor. price: 36 - slot: 21 - category: armor + slot: 12 + group: armor + category: combat + thorns: id: enchanted_book name: Thorns @@ -204,23 +140,45 @@ items: - Damages anyone who attacks you. - Maybe don't use it on yourself. price: 14 - slot: 24 + slot: 13 + category: combat protection_i: id: enchanted_book name: Protection I lore: - Gives you a 16% damage reduction. - price: 10 - slot: 25 - category: protection + price: 8 + slot: 14 + group: protection + category: combat protection_ii: id: enchanted_book name: Protection II lore: - Gives you a 32% damage reduction. - price: 20 - slot: 26 - category: protection + price: 16 + slot: 15 + group: protection + category: combat + protection_iii: + id: enchanted_book + name: Protection III + lore: + - Gives you a 48% damage reduction. + price: 24 + slot: 16 + group: protection + category: combat + protection_iv: + id: enchanted_book + name: Protection IV + lore: + - Gives you a 64% damage reduction. + price: 32 + slot: 17 + group: protection + category: combat + bow: id: bow name: Bow @@ -228,7 +186,9 @@ items: - A bow that shoots arrows. - Comes with a regenerating arrow. price: 8 - slot: 27 + slot: 18 + category: combat + arrow_1: id: arrow name: Arrow @@ -236,43 +196,49 @@ items: - A basic arrow. - Regenerates after 3 seconds. price: 3 - slot: 28 - category: arrow + slot: 19 + group: arrow + category: combat arrow_2: id: arrow - name: Arrow + name: 2x Arrow lore: - 2 arrows. - Regenerates after 3 seconds. price: 6 - slot: 29 - category: arrow + slot: 20 + group: arrow amount: 2 + category: combat arrow_3: id: arrow - name: Arrow + name: 3x Arrow lore: - 3 arrows. - Regenerates after 3 seconds. price: 9 - slot: 30 - category: arrow + 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: 31 + slot: 22 + category: combat power_i: id: enchanted_book name: Power I lore: - Increases your damage by 50%, kinda hurts. price: 8 - slot: 32 - category: power + slot: 23 + group: power + category: combat power_ii: id: enchanted_book name: Power II @@ -280,24 +246,382 @@ items: - Increases your damage by 75%. - Use it well, and you can one-shot someone. price: 12 - slot: 33 - category: power + slot: 24 + group: power + category: combat punch_i: id: enchanted_book name: Punch I lore: - Send them flying! price: 6 - slot: 34 - category: punch + slot: 25 + group: punch + category: combat punch_ii: id: enchanted_book name: Punch II lore: - Can't touch this! price: 10 - slot: 35 - category: punch + 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 + + 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: + - A flint and steel, the same thing that ooga-booga used. + - Great if you want to get arrested for arson. + 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 + + # 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 Potion of 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 Potion of 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 Potion of Regeneration V + lore: + - Restores 3 per second. + - Certified LEGA (Liquid Enchanted Golden Apples). + price: 14 + slot: 8 + amount: 2 + category: potion + + speed_potion: + id: potion + name: 3x Potion of Speed II + lore: + - Speeds you up for 15 seconds. + - Perfect for catching goons. + price: 6 + slot: 9 + amount: 3 + category: potion + + 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: 10 + amount: 3 + category: potion + + # Miscellaneous Items tracker: id: compass name: Tracker @@ -305,7 +629,9 @@ items: - Right click to point to the nearest player. - Has a 5-second cooldown and alerts the tracked player. price: 6 - slot: 36 + slot: 0 + category: miscellaneous + steal_perk: id: bundle name: Steal Perk @@ -313,8 +639,9 @@ items: - 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 + slot: 1 + group: perk + category: miscellaneous heal_perk: id: red_dye name: Heal Perk @@ -322,8 +649,9 @@ items: - You will heal to full health when you kill a player. - The perk is kept after use. price: 7 - slot: 38 - category: perk + slot: 2 + group: perk + category: miscellaneous double_jump: id: rabbit_foot name: Double Jump @@ -331,26 +659,9 @@ items: - Gives you the ability to jump twice. - The perk is kept after use and has a 3 second cooldown. price: 10 - 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: 3 - slot: 44 - amount: 32 + slot: 3 + group: perk + category: miscellaneous spawn-locations: 0: - x: 52.5 @@ -402,6 +713,39 @@ spawn-locations: - 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 diff --git a/pgame-plugin/src/main/resources/paper-plugin.yml b/pgame-plugin/src/main/resources/paper-plugin.yml index 45eebee..05d8070 100644 --- a/pgame-plugin/src/main/resources/paper-plugin.yml +++ b/pgame-plugin/src/main/resources/paper-plugin.yml @@ -18,6 +18,9 @@ dependencies: load: BEFORE required: true join-classpath: true + My_Worlds: + load: BEFORE + required: false authors: - Mester description: The paper plugin for MesterNetwork's Party Games From 61f3545f54f573a7ec3e9826fbfc317f0bce5e06 Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Sat, 21 Jun 2025 09:32:41 +0200 Subject: [PATCH 15/18] fix compiler error --- .../info/mester/network/partygames/game/HealthShopMinigame.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index b37aff8..d0a8225 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -651,7 +651,7 @@ class HealthShopMinigame( // launch a fireball in the direction the player is looking val fireball = event.player.launchProjectile(Fireball::class.java) - fireball.isIncendiary = false + fireball.setIsIncendiary(false) fireball.yield = 4f fireball.velocity = event.player.location.direction From 14b0b49e984a2a6d19bf034cad4698e9d57a7f8a Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Sun, 22 Jun 2025 03:26:55 +0200 Subject: [PATCH 16/18] API: allow minigames to enable fall damage Plugin: - Add support for Party and Friends Health Shop: - Add Turtle Master potion - Add Feather Fall perk - Add testing health shop UI (/healthshop testui) --- .../mester/network/partygames/api/Minigame.kt | 9 +- .../partygames/api/PartyGamesListener.kt | 10 +- pgame-plugin/build.gradle.kts | 4 + pgame-plugin/src/health-shop-schema.json | 5 +- .../mester/network/partygames/Bootstrapper.kt | 34 +++- .../network/partygames/PartyListener.kt | 29 +++ .../partygames/game/DamageDealerMinigame.kt | 1 + .../partygames/game/GardeningMinigame.kt | 1 + .../partygames/game/HealthShopMinigame.kt | 100 +++++----- .../partygames/game/MineguessrMinigame.kt | 1 + .../network/partygames/game/QueueManager.kt | 52 +++++ .../partygames/game/SpeedBuildersMinigame.kt | 2 + .../game/healthshop/HealthShopItem.kt | 12 +- .../game/healthshop/HealthShopKit.kt | 11 ++ .../game/healthshop/HealthShopPlayerData.kt | 50 +---- .../game/healthshop/HealthShopUI.kt | 178 +++++++++++------- pgame-plugin/src/main/resources/config.yml | 2 +- .../src/main/resources/health-shop.yml | 173 ++++++++++++++--- .../src/main/resources/paper-plugin.yml | 8 + .../testminigame/PlaceBlockMinigame.kt | 1 + 20 files changed, 488 insertions(+), 195 deletions(-) diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index 636be27..66765eb 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -8,6 +8,7 @@ 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 @@ -17,6 +18,7 @@ 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 @@ -41,6 +43,7 @@ abstract class Minigame( protected val game: Game, minigameName: String, val introductionType: IntroductionType = IntroductionType.CIRCLE, + val allowFallDamage: Boolean = false, ) { constructor(game: Game, minigameName: String) : this(game, minigameName, IntroductionType.CIRCLE) @@ -86,7 +89,9 @@ abstract class Minigame( * * Can be used to set up the world (unlike in the constructor, where a world is not yet ready) */ - open fun onLoad() {} + open fun onLoad() { + startPos.world.setGameRule(GameRule.FALL_DAMAGE, allowFallDamage) + } /** * A function to finish the minigame (roll back any changes, handle scores, etc.) @@ -202,6 +207,8 @@ abstract class Minigame( open fun handleEntityDismount(event: EntityDismountEvent) {} + open fun handleEntityDamage(event: EntityDamageEvent) {} + open fun handleEntityDamageByEntity(event: EntityDamageByEntityEvent) {} open fun handleEntityRegainHealth(event: EntityRegainHealthEvent) {} diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt index ec7e7d4..f2d7889 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/PartyGamesListener.kt @@ -106,11 +106,15 @@ class PartyGamesListener( @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 diff --git a/pgame-plugin/build.gradle.kts b/pgame-plugin/build.gradle.kts index 5cd7994..017b8dc 100644 --- a/pgame-plugin/build.gradle.kts +++ b/pgame-plugin/build.gradle.kts @@ -17,6 +17,7 @@ repositories { 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 { @@ -43,6 +44,9 @@ dependencies { 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 { diff --git a/pgame-plugin/src/health-shop-schema.json b/pgame-plugin/src/health-shop-schema.json index 8def21a..629611c 100644 --- a/pgame-plugin/src/health-shop-schema.json +++ b/pgame-plugin/src/health-shop-schema.json @@ -53,7 +53,10 @@ "offhand", "regen_ii", "splash_healing_ii", - "splash_healing" + "splash_healing", + "speed_ii", + "jump_boost", + "turtle_master" ] }, "category": { 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 index 82f21e0..f1fdcf7 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/Bootstrapper.kt @@ -4,6 +4,8 @@ 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 @@ -13,12 +15,19 @@ 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) { @@ -105,7 +114,7 @@ class Bootstrapper : PluginBootstrap { ) return@executes 1 } - PartyGames.plugin.queueManager.joinQueue(bundle, listOf(sender)) + PartyGames.plugin.queueManager.joinQueue(bundle, sender) Command.SINGLE_SUCCESS }, ).build(), @@ -130,6 +139,29 @@ class Bootstrapper : PluginBootstrap { }, ).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(), + ) } } 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 index 2003d6c..ee48375 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyListener.kt @@ -8,16 +8,21 @@ 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 @@ -338,4 +343,28 @@ class PartyListener( 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/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt index a508892..467e9f5 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/DamageDealerMinigame.kt @@ -180,6 +180,7 @@ class DamageDealerMinigame( override fun onLoad() { game.world.setGameRule(GameRule.NATURAL_REGENERATION, false) + super.onLoad() } override fun start() { diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt index 5ba0251..e4411c2 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/GardeningMinigame.kt @@ -262,6 +262,7 @@ class GardeningMinigame( worldBorder.center = startPos worldBorder.size = 2 * MAP_RADIUS + 1.0 worldBorder.warningDistance = 0 + super.onLoad() } override fun start() { diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index d0a8225..9bbbb9d 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -39,26 +39,24 @@ 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 -import org.bukkit.event.inventory.InventoryClickEvent import org.bukkit.event.inventory.InventoryCloseEvent import org.bukkit.event.inventory.InventoryOpenEvent 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.inventory.ItemStack import org.bukkit.inventory.meta.CompassMeta import org.bukkit.persistence.PersistentDataType -import org.bukkit.scheduler.BukkitTask +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 @@ -89,12 +87,13 @@ class ShopFailedException( 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 supplyDrops: MutableList> = mutableListOf() - private var startingHealth: Double = 80.0 + var startingHealth: Double = 80.0 + private set private val plugin = PartyGames.plugin fun getShopItems(): List = shopItems.toList() @@ -251,7 +250,7 @@ class HealthShopMinigame( val survivedSeconds = survivedTicks / 20 // for every 20th second the player has survived, give them a point // 1 point every 10th second if the player is still alive (last player standing, time is up) - val survivedPoints = floor((survivedSeconds / 20).toDouble()).toInt() * (if (didSurvive) 2 else 1) + val survivedPoints = floor(survivedSeconds / if (didSurvive) 10.0 else 20.0).toInt() if (survivedPoints > 0) { game.addScore(player, survivedPoints, "Survived $survivedSeconds seconds") } @@ -266,6 +265,7 @@ class HealthShopMinigame( worldBorder.warningDistance = 2 worldBorder.damageBuffer = 0.0 worldBorder.damageAmount = 1.5 + super.onLoad() } override fun start() { @@ -358,13 +358,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 } } @@ -477,6 +482,14 @@ class HealthShopMinigame( event.player.sendMessage(Component.text("Left click to reopen the shop.", NamedTextColor.AQUA)) } + override fun handleEntityDamage(event: EntityDamageEvent) { + val player = event.entity as? Player ?: return + if (event.damageSource.damageType == DamageType.FALL && getPlayerData(player).featherFall) { + event.isCancelled = true + } + super.handleEntityDamage(event) + } + @Suppress("UnstableApiUsage") override fun handleEntityDamageByEntity(event: EntityDamageByEntityEvent) { if (state != HealthShopMinigameState.FIGHT) { @@ -698,32 +711,38 @@ class HealthShopMinigame( override fun handleBlockPlace(event: BlockPlaceEvent) { val block = event.block if (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 location = block.location.clone() + val world = block.world + val totalTime = 6 * 20 // 6 seconds in ticks - override fun accept(t: BukkitTask) { - if (!running) { - t.cancel() - return - } - remainingTime -= 1 - if (remainingTime <= 0) { - 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) } - } + object : BukkitRunnable() { + var remainingTime = totalTime + + override fun run() { + if (!running) { + cancel() + return } - }, - 0, - 1, - ) + + // Check if the block is still the expected type + if (world.getBlockAt(location).type != Material.OAK_PLANKS) { + cancel() + return + } + + 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 @@ -786,17 +805,6 @@ class HealthShopMinigame( } } - override fun handleInventoryClick( - event: InventoryClickEvent, - clickedInventory: Inventory, - ) { - val holder = clickedInventory.getHolder(false) - if (holder is HealthShopUI) { - event.isCancelled = true - holder.onInventoryClick(event) - } - } - override fun handlePlayerToggleFlight(event: PlayerToggleFlightEvent) { val player = event.player if (player.gameMode == GameMode.CREATIVE) { 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 index 64c0d0a..7459c5b 100644 --- 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 @@ -192,6 +192,7 @@ class MineguessrMinigame( override fun onLoad() { game.world.setGameRule(GameRule.REDUCED_DEBUG_INFO, true) + super.onLoad() } override fun start() { 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 index f4b5df6..5383a71 100644 --- 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 @@ -1,9 +1,12 @@ 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 @@ -12,6 +15,33 @@ 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 + } + } + + val pafPlayerManager: PAFPlayerManager by lazy { + if (partyAvailable) { + PAFPlayerManager.getInstance() + } else { + throw IllegalStateException("Party and Friends plugin is not available.") + } + } + val pafPartyManager: PartyManager by lazy { + if (partyAvailable) { + PartyManager.getInstance() + } else { + throw IllegalStateException("Party and Friends plugin is not available.") + } + } + } + private val core = plugin.core private val gameRegistry = core.gameRegistry private val queues = mutableMapOf() @@ -42,6 +72,28 @@ class QueueManager( } fun joinQueue( + bundle: MinigameBundle, + player: Player, + ) { + if (partyAvailable) { + val pafPlayer = pafPlayerManager.getPlayer(player.uniqueId) + val party = pafPartyManager.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, ) { diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt index 5a0e334..8e8ee62 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/SpeedBuildersMinigame.kt @@ -398,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) @@ -917,6 +918,7 @@ class SpeedBuildersMinigame( override fun onLoad() { game.world.difficulty = Difficulty.NORMAL + super.onLoad() } override fun start() { 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 index af512bf..450f921 100644 --- 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 @@ -28,7 +28,7 @@ enum class HealthShopItemCategory( /** * Miscellaneous items that do not fit into other categories. */ - MISCELLANEOUS(Material.ENDER_PEARL), + MISCELLANEOUS(Material.COMPASS), } class HealthShopItem( @@ -87,13 +87,19 @@ class HealthShopItem( HealthShopUI.setRegenPotion(item, false) } // apply speed potion to item - if (key == "speed_potion") { + if (group == "speed_ii") { HealthShopUI.setSpeedPotion(item, false) } // apply jump potion to item - if (key == "jump_potion") { + 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) + } return HealthShopItem( item, 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 index c86d355..10119fc 100644 --- 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 @@ -30,6 +30,17 @@ data class HealthShopKit( ), ) } + 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) { 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 index 2df7b48..4bd608c 100644 --- 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 @@ -1,57 +1,9 @@ package info.mester.network.partygames.game.healthshop -import info.mester.network.partygames.PartyGames -import org.bukkit.NamespacedKey -import org.bukkit.persistence.PersistentDataAdapterContext -import org.bukkit.persistence.PersistentDataContainer -import org.bukkit.persistence.PersistentDataType - data class HealthShopPlayerData( var maxArrows: Int = 0, var stealPerk: Boolean = false, var healPerk: Boolean = false, var doubleJump: Boolean = false, + var featherFall: Boolean = false, ) - -class HealthShopPlayerDataType : PersistentDataType { - companion object { - private val plugin = PartyGames.plugin - - private val MAX_ARROWS_KEY = NamespacedKey(plugin, "max_arrows") - private val STEAL_PERK_KEY = NamespacedKey(plugin, "steal_perk") - private val HEAL_PERK_KEY = NamespacedKey(plugin, "heal_perk") - private val DOUBLE_JUMP_KEY = NamespacedKey(plugin, "double_jump") - } - - override fun getPrimitiveType(): Class = PersistentDataContainer::class.java - - override fun getComplexType(): Class = HealthShopPlayerData::class.java - - override fun toPrimitive( - complex: HealthShopPlayerData, - context: PersistentDataAdapterContext, - ): PersistentDataContainer { - val container = context.newPersistentDataContainer() - container.set(MAX_ARROWS_KEY, PersistentDataType.INTEGER, complex.maxArrows) - container.set(STEAL_PERK_KEY, PersistentDataType.BOOLEAN, complex.stealPerk) - container.set(HEAL_PERK_KEY, PersistentDataType.BOOLEAN, complex.healPerk) - container.set(DOUBLE_JUMP_KEY, PersistentDataType.BOOLEAN, complex.doubleJump) - return container - } - - override fun fromPrimitive( - primitive: PersistentDataContainer, - context: PersistentDataAdapterContext, - ): HealthShopPlayerData { - val maxArrows = primitive.get(MAX_ARROWS_KEY, PersistentDataType.INTEGER) ?: 0 - val stealPerk = primitive.get(STEAL_PERK_KEY, PersistentDataType.BOOLEAN) ?: false - val healPerk = primitive.get(HEAL_PERK_KEY, PersistentDataType.BOOLEAN) ?: false - val doubleJump = primitive.get(DOUBLE_JUMP_KEY, PersistentDataType.BOOLEAN) ?: false - return HealthShopPlayerData( - maxArrows = maxArrows, - stealPerk = stealPerk, - healPerk = healPerk, - doubleJump = doubleJump, - ) - } -} 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 index 9de18c5..ce56f89 100644 --- 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 @@ -98,6 +98,23 @@ class HealthShopUI( } } + fun setTurtleMasterPotion( + item: ItemStack, + long: Boolean, + strong: Boolean, + ) { + item.editMeta(PotionMeta::class.java) { meta -> + applyGenericItemMeta(meta) + meta.basePotionType = + when { + !long && !strong -> PotionType.TURTLE_MASTER + long -> PotionType.LONG_TURTLE_MASTER + strong -> PotionType.STRONG_TURTLE_MASTER + else -> PotionType.TURTLE_MASTER // fallback, should not happen + } + } + } + fun setRegenPotion( item: ItemStack, withName: Boolean = true, @@ -113,7 +130,7 @@ class HealthShopUI( withName: Boolean = true, ) = setCustomPotion( item, - PotionEffect(PotionEffectType.SPEED, 15 * 20, 1, false), + PotionEffect(PotionEffectType.SPEED, 20 * 20, 1, false), Color.fromRGB(51, 235, 255), if (withName) "Speed" else null, ) @@ -493,54 +510,6 @@ class HealthShopUI( } player.inventory.setItem(EquipmentSlot.OFF_HAND, shield) } - // 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) - } - // process regeneration potion - 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.key == "speed_potion" }?.let { shopItem -> - val potion = ItemStack.of(Material.POTION) - setSpeedPotion(potion) - repeat(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) - repeat(shopItem.amount) { - player.inventory.addItem(potion) - } - } - // 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 armor kotlin .runCatching { @@ -586,25 +555,21 @@ class HealthShopUI( // 1 free arrow is included with the bow (which you need to buy an arrow) playerData.maxArrows = shopItem.amount + 1 } - // process tracker - if (purchasedItems.any { it.key == "tracker" }) { - val tracker = ItemStack.of(Material.COMPASS) - tracker.editMeta { meta -> - meta.setEnchantmentGlintOverride(false) + + // 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(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 + player.inventory.addItem(apple) } - // process double jump - if (purchasedItems.any { it.key == "double_jump" }) { - playerData.doubleJump = true + 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" }) { @@ -666,6 +631,87 @@ class HealthShopUI( } 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 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 + } // save this kit (index 8 is the last used kit) if (purchasedItems.isEmpty()) { diff --git a/pgame-plugin/src/main/resources/config.yml b/pgame-plugin/src/main/resources/config.yml index 60f0dda..3cf32d8 100644 --- a/pgame-plugin/src/main/resources/config.yml +++ b/pgame-plugin/src/main/resources/config.yml @@ -14,7 +14,7 @@ minigames: y: 62.0 z: 0.5 class: info.mester.network.partygames.game.HealthShopMinigame - display-name: Health Shop + display-name: Health Shop v1.1 speedbuilders: worlds: - world: mg-speedbuilders diff --git a/pgame-plugin/src/main/resources/health-shop.yml b/pgame-plugin/src/main/resources/health-shop.yml index a2ec187..c52d801 100644 --- a/pgame-plugin/src/main/resources/health-shop.yml +++ b/pgame-plugin/src/main/resources/health-shop.yml @@ -79,6 +79,7 @@ items: - May cause a bit of pain. price: 12 slot: 7 + amount: 3 group: sharpness category: combat sharpness_5: @@ -89,6 +90,7 @@ items: - OUCH! price: 18 slot: 8 + amount: 5 group: sharpness category: combat @@ -96,8 +98,8 @@ items: id: chainmail_chestplate name: Chainmail Armor lore: - - A basic chainmail armor. - - Good for blocking attacks. + - Surprisingly protective for interlinked metal rings. + - Medieval drip, now in PvP! price: 8 slot: 9 group: armor @@ -106,8 +108,8 @@ items: id: iron_chestplate name: Iron Armor lore: - - A basic iron armor. - - Good for blocking attacks. + - Reliable iron armor, forged for battle. + - Rusts slightly under pressure. price: 12 slot: 10 group: armor @@ -116,8 +118,8 @@ items: id: diamond_chestplate name: Diamond Armor lore: - - A basic diamond armor. - - Good for blocking attacks. + - Shiny, durable, and suspiciously flashy. + - Perfect for flexing and surviving. price: 24 slot: 11 group: armor @@ -126,8 +128,8 @@ items: id: netherite_chestplate name: Netherite Armor lore: - - The strongest armor in existence, imbued with ancient power. - - Only the bravest wield this mighty armor. + - The pinnacle of armor technology. + - 'Warning: may cause overconfidence.' price: 36 slot: 12 group: armor @@ -147,6 +149,7 @@ items: name: Protection I lore: - Gives you a 16% damage reduction. + - Every little bit helps, right? price: 8 slot: 14 group: protection @@ -156,8 +159,10 @@ items: 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: @@ -165,8 +170,10 @@ items: 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: @@ -174,8 +181,10 @@ items: 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 @@ -193,7 +202,7 @@ items: id: arrow name: Arrow lore: - - A basic arrow. + - A single arrow for precise shots. - Regenerates after 3 seconds. price: 3 slot: 19 @@ -203,7 +212,7 @@ items: id: arrow name: 2x Arrow lore: - - 2 arrows. + - Two arrows. Double the chances to whiff. - Regenerates after 3 seconds. price: 6 slot: 20 @@ -214,7 +223,7 @@ items: id: arrow name: 3x Arrow lore: - - 3 arrows. + - A polite way to say “leave.” - Regenerates after 3 seconds. price: 9 slot: 21 @@ -311,6 +320,15 @@ items: 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 @@ -349,8 +367,8 @@ items: 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. + - Sets things on fire. Intentionally. + - Technically not arson if it’s in a video game. price: 5 slot: 10 amount: 1 @@ -491,6 +509,17 @@ items: 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 @@ -599,26 +628,112 @@ items: amount: 2 category: potion - speed_potion: + speed_ii_2: id: potion - name: 3x Potion of Speed II + name: 2x Potion of Speed II lore: - - Speeds you up for 15 seconds. - - Perfect for catching goons. - price: 6 + - Boosts your speed for 20 seconds. + - Why walk when you can run like a panic attack? + price: 4 + amount: 2 slot: 9 - amount: 3 + group: speed_ii category: potion - jump_potion: + speed_ii_4: id: potion - name: 3x Potion of Jump Boost IV + name: 4x Potion of Speed II lore: - - Turns you into a rabbit for 20 seconds. - - Boing-boing-boing! + - Boosts your speed for 20 seconds. + - Basically caffeine in a bottle. price: 6 + amount: 4 slot: 10 - amount: 3 + group: speed_ii + category: potion + + speed_ii_6: + id: potion + name: 6x Potion of 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 Potion of 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 Potion of Jump Boost IV + lore: + - Leap up to 4 blocks for 20 seconds. + - You are now 50% rabbit. + price: 6 + amount: 4 + slot: 13 + group: jump_boost + category: potion + + jump_boost_6: + id: potion + name: 6x Potion of 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 Potion of the Turtle Master + lore: + - Gain Resistance III & Slowness IV. + - Become invincible… eventually. + price: 8 + amount: 2 + slot: 15 + group: turtle_master + category: potion + + turtle_master_long: + id: potion + name: 2x Long Potion of the Turtle Master + lore: + - Same effects, now with 40% 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 Potion of the Turtle Master + lore: + - Resistance IV & Slowness VI. + - You are now a tank with two left feet. + price: 16 + amount: 2 + slot: 17 + group: turtle_master category: potion # Miscellaneous Items @@ -662,6 +777,16 @@ items: 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 + spawn-locations: 0: - x: 52.5 diff --git a/pgame-plugin/src/main/resources/paper-plugin.yml b/pgame-plugin/src/main/resources/paper-plugin.yml index 05d8070..4f8c381 100644 --- a/pgame-plugin/src/main/resources/paper-plugin.yml +++ b/pgame-plugin/src/main/resources/paper-plugin.yml @@ -21,6 +21,14 @@ dependencies: 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/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt b/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt index 7f5554a..bd6036a 100644 --- a/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt +++ b/test-minigame/src/main/kotlin/info/mester/network/testminigame/PlaceBlockMinigame.kt @@ -16,6 +16,7 @@ class PlaceBlockMinigame( val world = game.world world.worldBorder.size = 30.0 world.worldBorder.center = startPos + super.onLoad() } override fun start() { From a6bfcdff63cc0d88ac196fb4c669ead285c424be Mon Sep 17 00:00:00 2001 From: MesterMan03 Date: Tue, 24 Jun 2025 00:24:31 +0200 Subject: [PATCH 17/18] API: worlds can have display name Plugin: Health Shop: - Added posion, blindness and levitation potions - Tweaked world border --- .../mester/network/partygames/api/Game.kt | 2 +- .../network/partygames/api/GameRegistry.kt | 1 + .../mester/network/partygames/api/Minigame.kt | 6 +- pgame-plugin/src/health-shop-schema.json | 5 +- .../mester/network/partygames/PartyGames.kt | 3 +- .../partygames/game/HealthShopMinigame.kt | 23 ++- .../network/partygames/game/QueueManager.kt | 19 ++- .../game/healthshop/HealthShopItem.kt | 14 +- .../game/healthshop/HealthShopUI.kt | 151 ++++++++++++++++-- .../sidebar/GameSidebarComponent.kt | 6 + .../src/main/resources/health-shop.yml | 119 ++++++++++---- 11 files changed, 288 insertions(+), 61 deletions(-) diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt index 54b5e08..beb0d16 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Game.kt @@ -302,7 +302,7 @@ class Game( // start an async task to load the world 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( 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 index eed4c6b..ef83709 100644 --- 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 @@ -12,6 +12,7 @@ data class MinigameWorld( val startPos: Vector, val yaw: Float, val pitch: Float, + val displayName: String? = null, ) { constructor(name: String, startPos: Vector) : this( name, diff --git a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt index 66765eb..6fd5e3f 100644 --- a/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt +++ b/pgame-api/src/main/kotlin/info/mester/network/partygames/api/Minigame.kt @@ -59,7 +59,7 @@ abstract class Minigame( pos.world = Bukkit.getWorld(game.worldName)!! return pos } - val rootWorldName: String + val rootWorld: MinigameWorld val worldIndex: Int val originalPlugin: JavaPlugin @@ -68,8 +68,8 @@ abstract class Minigame( val minigameConfig = core.gameRegistry.getMinigame(minigameName)!! originalPlugin = minigameConfig.plugin worldIndex = Random.nextInt(0, minigameConfig.worlds.size) - rootWorldName = minigameConfig.worlds[worldIndex].name - startPos = minigameConfig.worlds[worldIndex].toLocation(Bukkit.getWorld(rootWorldName)!!) + rootWorld = minigameConfig.worlds[worldIndex] + startPos = minigameConfig.worlds[worldIndex].toLocation(Bukkit.getWorld(rootWorld.name)!!) } /** diff --git a/pgame-plugin/src/health-shop-schema.json b/pgame-plugin/src/health-shop-schema.json index 629611c..9ab73a8 100644 --- a/pgame-plugin/src/health-shop-schema.json +++ b/pgame-plugin/src/health-shop-schema.json @@ -56,7 +56,10 @@ "splash_healing", "speed_ii", "jump_boost", - "turtle_master" + "turtle_master", + "levitation", + "blindness", + "poison" ] }, "category": { diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt index 4f84078..e887131 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/PartyGames.kt @@ -167,7 +167,8 @@ class PartyGames : JavaPlugin() { val z = entry["z"] as Double val yaw = entry["yaw"] as? Double ?: 0.0 val pitch = entry["pitch"] as? Double ?: 0.0 - MinigameWorld(world, Vector(x, y, z), yaw.toFloat(), pitch.toFloat()) + val displayName = entry["display-name"] as? String + MinigameWorld(world, Vector(x, y, z), yaw.toFloat(), pitch.toFloat(), displayName) } else { null } diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index 9bbbb9d..c635e06 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -336,8 +336,27 @@ class HealthShopMinigame( } // 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) + // shrink the world border to close to 3 blocks in the last 15 seconds + startPos.world.worldBorder.setSize(3.0, TimeUnit.SECONDS, 3 * 60 - 20) + // 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(-8, 8) + val z = worldBorder.center.z + Random.nextInt(-8, 8) + worldBorder.setCenter(x, z) + worldBorder.damageAmount = 3.0 + worldBorder.damageBuffer = 4.0 + }, + 3 * 60 - 20, + 50, + ) } override fun finish() { 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 index 5383a71..894eaf0 100644 --- 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 @@ -26,20 +26,19 @@ class QueueManager( } } - val pafPlayerManager: PAFPlayerManager by lazy { + fun getPartyManager(): PartyManager? = if (partyAvailable) { - PAFPlayerManager.getInstance() + PartyManager.getInstance() } else { - throw IllegalStateException("Party and Friends plugin is not available.") + null } - } - val pafPartyManager: PartyManager by lazy { + + fun getPAFPlayerManager(): PAFPlayerManager? = if (partyAvailable) { - PartyManager.getInstance() + PAFPlayerManager.getInstance() } else { - throw IllegalStateException("Party and Friends plugin is not available.") + null } - } } private val core = plugin.core @@ -76,8 +75,8 @@ class QueueManager( player: Player, ) { if (partyAvailable) { - val pafPlayer = pafPlayerManager.getPlayer(player.uniqueId) - val party = pafPartyManager.getParty(pafPlayer) + val pafPlayer = getPAFPlayerManager()!!.getPlayer(player.uniqueId) + val party = getPartyManager()!!.getParty(pafPlayer) if (party != null) { if (party.leader.uniqueId != player.uniqueId) { Audience.audience(player).sendMessage( 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 index 450f921..4ed3d1b 100644 --- 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 @@ -98,7 +98,19 @@ class HealthShopItem( if (group == "turtle_master") { val long = key.endsWith("_long") val strong = key.endsWith("_strong") - HealthShopUI.setTurtleMasterPotion(item, long, 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( 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 index ce56f89..12047f8 100644 --- 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 @@ -12,8 +12,10 @@ 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 @@ -31,6 +33,7 @@ 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( @@ -55,17 +58,20 @@ class HealthShopUI( private fun setCustomPotion( item: ItemStack, - potionEffect: PotionEffect, + potionEffects: List, color: Color, potionName: String?, + showExtraData: Boolean = true, ) { item.editMeta(PotionMeta::class.java) { meta -> applyGenericItemMeta(meta) - meta.addCustomEffect(potionEffect, true) + potionEffects.forEach { potionEffect -> + meta.addCustomEffect(potionEffect, true) + } meta.color = color - val duration = potionEffect.duration / 20 + val duration = potionEffects.minBy { it.duration }.duration / 20 val minutes = duration / 60 val seconds = String.format("%02d", duration % 60) @@ -74,13 +80,65 @@ class HealthShopUI( MiniMessage .miniMessage() .deserialize( - "$potionName ${(potionEffect.amplifier + 1).toRomanNumeral()} ($minutes:$seconds)", + 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, @@ -102,19 +160,58 @@ class HealthShopUI( item: ItemStack, long: Boolean, strong: Boolean, + withName: Boolean = true, ) { - item.editMeta(PotionMeta::class.java) { meta -> - applyGenericItemMeta(meta) - meta.basePotionType = - when { - !long && !strong -> PotionType.TURTLE_MASTER - long -> PotionType.LONG_TURTLE_MASTER - strong -> PotionType.STRONG_TURTLE_MASTER - else -> PotionType.TURTLE_MASTER // fallback, should not happen - } - } + 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, @@ -477,7 +574,7 @@ class HealthShopUI( } purchasedItems.firstOrNull { it.key.startsWith("sharpness_") }.let { sharpnessItem -> if (sharpnessItem != null) { - val sharpness = sharpnessItem.key.substringAfter("sharpness_").toInt() + val sharpness = sharpnessItem.amount meta.addEnchant(Enchantment.SHARPNESS, sharpness, true) } } @@ -687,6 +784,30 @@ class HealthShopUI( 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" }) { diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt index 992756e..27b09a4 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/sidebar/GameSidebarComponent.kt @@ -50,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) } @@ -57,6 +60,9 @@ 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 stars: ${game.playerData(player)!!.stars}★")) drawable.drawLine(mm.deserialize("Your current score: ${game.playerData(player)!!.score}")) diff --git a/pgame-plugin/src/main/resources/health-shop.yml b/pgame-plugin/src/main/resources/health-shop.yml index c52d801..6462ecb 100644 --- a/pgame-plugin/src/main/resources/health-shop.yml +++ b/pgame-plugin/src/main/resources/health-shop.yml @@ -71,26 +71,26 @@ items: slot: 6 category: combat - sharpness_3: + sharpness_5: id: enchanted_book - name: Sharpness III + name: Sharpness V lore: - - Increases your damage by 1♥ + - Increases your damage by 1.5♥ - May cause a bit of pain. - price: 12 + price: 10 slot: 7 - amount: 3 + amount: 5 group: sharpness category: combat - sharpness_5: + sharpness_10: id: enchanted_book - name: Sharpness V + name: Sharpness X lore: - - Increases your damage by 1.5♥ + - Increases your damage by 2.75♥ - OUCH! price: 18 slot: 8 - amount: 5 + amount: 10 group: sharpness category: combat @@ -595,7 +595,7 @@ items: regen_ii_2: id: potion - name: 2x Potion of Regeneration II + name: 2x Regeneration II lore: - Restores 2 every 5 seconds. - Slow but steady wins... maybe. @@ -607,7 +607,7 @@ items: regen_ii_4: id: potion - name: 4x Potion of Regeneration II + name: 4x Regeneration II lore: - Restores 2 every 5 seconds. - Patience is a virtue. Healing is a luxury. @@ -619,7 +619,7 @@ items: regen_v: id: potion - name: 2x Potion of Regeneration V + name: 2x Regeneration V lore: - Restores 3 per second. - Certified LEGA (Liquid Enchanted Golden Apples). @@ -630,7 +630,7 @@ items: speed_ii_2: id: potion - name: 2x Potion of Speed II + name: 2x Speed II lore: - Boosts your speed for 20 seconds. - Why walk when you can run like a panic attack? @@ -642,7 +642,7 @@ items: speed_ii_4: id: potion - name: 4x Potion of Speed II + name: 4x Speed II lore: - Boosts your speed for 20 seconds. - Basically caffeine in a bottle. @@ -654,7 +654,7 @@ items: speed_ii_6: id: potion - name: 6x Potion of Speed II + name: 6x Speed II lore: - Boosts your speed for 20 seconds. - Wanna break the sound barrier? @@ -666,7 +666,7 @@ items: jump_boost_2: id: potion - name: 2x Potion of Jump Boost IV + name: 2x Jump Boost IV lore: - Leap up to 4 blocks for 20 seconds. - Gravity? Never heard of her. @@ -678,10 +678,10 @@ items: jump_boost_4: id: potion - name: 4x Potion of Jump Boost IV + name: 4x Jump Boost IV lore: - Leap up to 4 blocks for 20 seconds. - - You are now 50% rabbit. + - Kangaroos' favourite morning drink. price: 6 amount: 4 slot: 13 @@ -690,7 +690,7 @@ items: jump_boost_6: id: potion - name: 6x Potion of Jump Boost IV + name: 6x Jump Boost IV lore: - Leap up to 4 blocks for 20 seconds. - Built for players who fear stairs. @@ -702,33 +702,29 @@ items: turtle_master: id: potion - name: 2x Potion of the Turtle Master + name: 2x Turtle Master lore: - - Gain Resistance III & Slowness IV. - Become invincible… eventually. price: 8 amount: 2 slot: 15 group: turtle_master category: potion - turtle_master_long: id: potion - name: 2x Long Potion of the Turtle Master + name: 2x Long Turtle Master lore: - - Same effects, now with 40% more suffering. + - 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 Potion of the Turtle Master + name: 2x Strong Turtle Master lore: - - Resistance IV & Slowness VI. - You are now a tank with two left feet. price: 16 amount: 2 @@ -736,6 +732,75 @@ items: 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 From 04b4a6ee21c8cb4fa065413b68758476f9eb479e Mon Sep 17 00:00:00 2001 From: Mester Date: Sun, 4 Jan 2026 15:36:18 +0100 Subject: [PATCH 18/18] Health Shop v1.1 --- .../partygames/game/HealthShopMinigame.kt | 109 +++++++++++++++--- .../game/healthshop/HealthShopPlayerData.kt | 1 + .../game/healthshop/HealthShopUI.kt | 4 + .../src/main/resources/health-shop.yml | 10 ++ 4 files changed, 106 insertions(+), 18 deletions(-) diff --git a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt index c635e06..627fccb 100644 --- a/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt +++ b/pgame-plugin/src/main/kotlin/info/mester/network/partygames/game/HealthShopMinigame.kt @@ -50,8 +50,12 @@ 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.potion.PotionEffect +import org.bukkit.potion.PotionEffectType import org.bukkit.scheduler.BukkitRunnable import org.bukkit.util.Vector import java.io.File @@ -92,6 +96,7 @@ class HealthShopMinigame( private val shopItems: MutableList = mutableListOf() private val startLocations: MutableMap> = mutableMapOf() private val supplyDrops: MutableList> = mutableListOf() + private val gameTimes = mutableMapOf() var startingHealth: Double = 80.0 private set private val plugin = PartyGames.plugin @@ -104,8 +109,9 @@ class HealthShopMinigame( 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 { @@ -116,15 +122,15 @@ 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<*, *>) { @@ -148,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<*, *>) { @@ -156,6 +176,7 @@ class HealthShopMinigame( supplyDrops.add(WeightedItem(key, weight)) } } + startingHealth = config.getDouble("health", 80.0) } } @@ -309,6 +330,7 @@ class HealthShopMinigame( state = HealthShopMinigameState.FIGHT 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 @@ -324,20 +346,40 @@ class HealthShopMinigame( // time to give the items! :) player.inventory.clear() 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 = 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 * 20) { + + // 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 close to 3 blocks in the last 15 seconds - startPos.world.worldBorder.setSize(3.0, TimeUnit.SECONDS, 3 * 60 - 20) + 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, @@ -348,13 +390,13 @@ class HealthShopMinigame( } val worldBorder = startPos.world.worldBorder - val x = worldBorder.center.x + Random.nextInt(-8, 8) - val z = worldBorder.center.z + Random.nextInt(-8, 8) + val x = worldBorder.center.x + Random.nextInt(-3, 3) + val z = worldBorder.center.z + Random.nextInt(-3, 3) worldBorder.setCenter(x, z) - worldBorder.damageAmount = 3.0 - worldBorder.damageBuffer = 4.0 + worldBorder.damageAmount = 4.5 + worldBorder.damageBuffer = 0.0 }, - 3 * 60 - 20, + (gameTime) * 20 + 50L, 50, ) } @@ -501,9 +543,14 @@ 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 - if (event.damageSource.damageType == DamageType.FALL && getPlayerData(player).featherFall) { + 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) @@ -669,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 @@ -725,6 +782,22 @@ class HealthShopMinigame( 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) { @@ -860,7 +933,7 @@ class HealthShopMinigame( player.playSound(sound, Sound.Emitter.self()) } - override val name = Component.text("Health Shop", 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!", 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 index 4bd608c..d86219a 100644 --- 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 @@ -6,4 +6,5 @@ data class HealthShopPlayerData( 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 index 12047f8..8f1d114 100644 --- 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 @@ -833,6 +833,10 @@ class HealthShopUI( 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()) { diff --git a/pgame-plugin/src/main/resources/health-shop.yml b/pgame-plugin/src/main/resources/health-shop.yml index 6462ecb..4e163f7 100644 --- a/pgame-plugin/src/main/resources/health-shop.yml +++ b/pgame-plugin/src/main/resources/health-shop.yml @@ -851,6 +851,16 @@ items: 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: