diff --git a/build.gradle.kts b/build.gradle.kts index 6b477f1..763ec36 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,11 +50,13 @@ repositories { mavenCentral() // Import the Maven Central Maven Repository. gradlePluginPortal() // Import the Gradle Plugin Portal Maven Repository. maven { url = uri("https://repo.purpurmc.org/snapshots") } // Import the PurpurMC Maven Repository. + maven { url = uri("https://repo.lucko.me/") } // Import the LuckPerms Maven Repository. } /* ---------------------- Java project deps ---------------------------- */ dependencies { compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") // Declare Purpur API version to be packaged. + compileOnly("net.luckperms:api:5.5") // Declare LuckPerms API for prefix/name color lookups. implementation("org.jetbrains.kotlin:kotlin-stdlib") // Import Kotlin standard library. implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") // Import Kotlin async library. } diff --git a/settings.gradle.kts b/settings.gradle.kts index 68bc5d9..7dd0522 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -rootProject.name = "MobHeads-OG" \ No newline at end of file +rootProject.name = "MobHeads-OG" diff --git a/src/main/kotlin/net/trueog/mobheadsog/Broadcast.kt b/src/main/kotlin/net/trueog/mobheadsog/Broadcast.kt new file mode 100644 index 0000000..336f4bf --- /dev/null +++ b/src/main/kotlin/net/trueog/mobheadsog/Broadcast.kt @@ -0,0 +1,102 @@ +package net.trueog.mobheadsog + +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.MiniMessage +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer +import net.luckperms.api.LuckPermsProvider +import org.bukkit.Bukkit +import org.bukkit.entity.Creeper +import org.bukkit.entity.Entity +import org.bukkit.entity.EntityType +import org.bukkit.entity.Player + +object Broadcast { + private val mm = MiniMessage.miniMessage() + private val legacy = LegacyComponentSerializer.builder().character('&').hexColors().build() + + /** Announce that [killer] beheaded [entity] to every online player in a whitelisted world. */ + fun beheaded(killer: Player, entity: Entity) { + val config = MobHeadsOG.config + if (!config.broadcastEnabled) { + return + } + if (config.broadcastWorlds.isEmpty()) { + return + } + + val rawPrefix = (getPrefix(killer) ?: "").replace('§', '&') + val prefix = legacy.deserialize(rawPrefix) + val name = legacy.deserialize(trailingColor(rawPrefix) + killer.name) + val mob = Component.text(mobName(entity)) + + val message = + mm.deserialize( + config.broadcastMessage, + Placeholder.component("prefix", prefix), + Placeholder.component("name", name), + Placeholder.component("mob", mob), + ) + + for (player in Bukkit.getOnlinePlayers()) { + if (player.world.name in config.broadcastWorlds) { + player.sendMessage(message) + } + } + } + + /** @return The player's LuckPerms prefix, or null if LuckPerms is absent or the player has none. */ + private fun getPrefix(player: Player): String? { + if (Bukkit.getPluginManager().getPlugin("LuckPerms") == null) { + return null + } + return try { + val luckPerms = LuckPermsProvider.get() + val user = luckPerms.getPlayerAdapter(Player::class.java).getUser(player) + user.cachedData.metaData.prefix + } catch (_: Exception) { + null + } + } + + /** @return The last color/hex code (with its '&') in [raw], or an empty string if there is none. */ + private fun trailingColor(raw: String): String { + var result = "" + var i = 0 + while (i < raw.length) { + if (raw[i] != '&' || i + 1 >= raw.length) { + i++ + continue + } + val code = raw[i + 1].lowercaseChar() + when { + code == '#' && i + 7 < raw.length -> { + result = raw.substring(i, i + 8) // &#rrggbb + i += 8 + } + code in "0123456789abcdef" -> { + result = raw.substring(i, i + 2) + i += 2 + } + code == 'r' -> { + result = "" // Reset clears the active color. + i += 2 + } + code in "klmno" -> i += 2 // Formatting codes leave the color untouched. + else -> i++ + } + } + return result + } + + /** @return A human readable name for [entity] to display in the broadcast. */ + private fun mobName(entity: Entity): String { + if (entity.type == EntityType.CREEPER) { + return if ((entity as Creeper).isPowered) "Charged Creeper" else "Creeper" + } + return Heads.getNameForEntity(entity) + ?: entity.type.name.split("_").joinToString(" ") { word -> + word.lowercase().replaceFirstChar(Char::uppercase) + } + } +} diff --git a/src/main/kotlin/net/trueog/mobheadsog/Config.kt b/src/main/kotlin/net/trueog/mobheadsog/Config.kt index 8c37bd2..4fb6577 100644 --- a/src/main/kotlin/net/trueog/mobheadsog/Config.kt +++ b/src/main/kotlin/net/trueog/mobheadsog/Config.kt @@ -9,6 +9,14 @@ class Config { var chargedCreeperChance: Double = 0.0 + val beheadWorlds: MutableList = mutableListOf() + + var broadcastEnabled: Boolean = false + + val broadcastWorlds: MutableList = mutableListOf() + + var broadcastMessage: String = "" + /** @return True if successful */ fun load(): Boolean { val file = File(MobHeadsOG.plugin.dataFolder, "config.yml") @@ -17,6 +25,28 @@ class Config { } val config = YamlConfiguration.loadConfiguration(file) + val beheadSection = config.getConfigurationSection("behead") + if (beheadSection == null) { + MobHeadsOG.plugin.logger.severe("Couldn't find a valid behead section in the config") + return false + } + beheadWorlds.clear() + beheadWorlds.addAll(beheadSection.getStringList("worlds")) + + val broadcastSection = config.getConfigurationSection("broadcast") + if (broadcastSection == null) { + MobHeadsOG.plugin.logger.severe("Couldn't find a valid broadcast section in the config") + return false + } + broadcastEnabled = broadcastSection.getBoolean("enabled") + broadcastWorlds.clear() + broadcastWorlds.addAll(broadcastSection.getStringList("worlds")) + broadcastMessage = broadcastSection.getString("message") ?: "" + if (broadcastEnabled && broadcastMessage.isEmpty()) { + MobHeadsOG.plugin.logger.severe("Couldn't find a valid broadcast message in the config") + return false + } + val dropChancesSection = config.getConfigurationSection("dropChances") if (dropChancesSection == null) { MobHeadsOG.plugin.logger.severe("Couldn't find a valid dropChances section in the config") diff --git a/src/main/kotlin/net/trueog/mobheadsog/Events.kt b/src/main/kotlin/net/trueog/mobheadsog/Events.kt index 770c61b..40744c0 100644 --- a/src/main/kotlin/net/trueog/mobheadsog/Events.kt +++ b/src/main/kotlin/net/trueog/mobheadsog/Events.kt @@ -39,6 +39,10 @@ class Events : Listener { return } + if (event.entity.world.name !in MobHeadsOG.config.beheadWorlds) { + return + } + val dropChance = if (event.entityType == EntityType.CREEPER && (event.entity as Creeper).isPowered) { MobHeadsOG.config.chargedCreeperChance @@ -54,16 +58,20 @@ class Events : Listener { return } + val killer = event.entity.killer as Player + when (event.entityType) { EntityType.SKELETON -> { val skull = ItemStack(Material.SKELETON_SKULL, 1) event.drops.add(skull) + Broadcast.beheaded(killer, event.entity) return } EntityType.ZOMBIE -> { val head = ItemStack(Material.ZOMBIE_HEAD, 1) event.drops.add(head) + Broadcast.beheaded(killer, event.entity) return } @@ -71,6 +79,7 @@ class Events : Listener { if (!(event.entity as Creeper).isPowered) { val head = ItemStack(Material.CREEPER_HEAD, 1) event.drops.add(head) + Broadcast.beheaded(killer, event.entity) return } } @@ -98,6 +107,7 @@ class Events : Listener { headMeta.playerProfile = profile head.itemMeta = headMeta event.drops.add(head) + Broadcast.beheaded(killer, event.entity) } @EventHandler diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 19a9c66..08cacb4 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,3 +1,20 @@ +behead: + # Heads only drop in these worlds. + worlds: + - world + - world_nether + - world_the_end + +broadcast: + enabled: true + # Only players in these worlds see the broadcast. + worlds: + - world + - world_nether + - world_the_end + # Placeholders: , , . + message: "☠ decapitated a !" + dropChances: allay: 0.025 axolotl: 0.025 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 04cad9e..bc4f729 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -2,3 +2,5 @@ name: MobHeads-OG version: '${version}' main: net.trueog.mobheadsog.MobHeadsOG api-version: '${apiVersion}' +softdepend: + - LuckPerms