Skip to content

Instantly share code, notes, and snippets.

@NigelThorne
Last active December 24, 2025 01:27
Show Gist options
  • Select an option

  • Save NigelThorne/b78dd17387e759ec2b08753d4973467d to your computer and use it in GitHub Desktop.

Select an option

Save NigelThorne/b78dd17387e759ec2b08753d4973467d to your computer and use it in GitHub Desktop.
Grave MInecraft Kite Script Plugin (v1)
// Component/Text Helpers
// Functions for working with Adventure text components
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
import net.kyori.adventure.text.minimessage.MiniMessage
import org.bukkit.entity.Player
/**
* Create a simple text component from a string
*
* @param message The message text
* @return A text component
*/
fun textComponent(message: String): Component = Component.text(message)
/**
* Create an empty component
*
* @return An empty text component
*/
fun emptyComponent(): Component = Component.empty()
/**
* Convert legacy (§-based) formatting to a modern Component
*
* @param legacy String with legacy § colour codes
* @return A component with proper formatting
*/
fun componentFromLegacy(legacy: String): Component {
return LegacyComponentSerializer.legacySection().deserialize(legacy)
}
/**
* Send an action bar message using legacy (§-based) formatting
*
* @param player The player to send the action bar to
* @param message Message with legacy § colour codes
*/
fun sendActionBarLegacy(player: Player, message: String) {
player.sendActionBar(LegacyComponentSerializer.legacySection().deserialize(message))
}
/**
* Send an action bar message using MiniMessage formatting
*
* @param player The player to send the action bar to
* @param message Message with MiniMessage tags
*/
fun sendActionBarMiniMessage(player: Player, message: String) {
player.sendActionBar(MiniMessage.miniMessage().deserialize(message))
}
// Graves System - Visual graves with items, XP, and holograms
// Players get a grave with their head when they die
@file:Import("common/logging.kite.kts")
@file:Import("common/storage.kite.kts")
@file:Import("common/component.kite.kts")
@file:Import("common/player.kite.kts")
@file:Import("common/sound.kite.kts")
import net.kyori.adventure.text.minimessage.MiniMessage
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.entity.ArmorStand
import org.bukkit.entity.ExperienceOrb
import org.bukkit.entity.Display
import org.bukkit.entity.Player
import org.bukkit.event.entity.PlayerDeathEvent
import org.bukkit.event.player.PlayerInteractAtEntityEvent
import org.bukkit.event.inventory.InventoryCloseEvent
import org.bukkit.inventory.Inventory
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.SkullMeta
import org.bukkit.configuration.file.YamlConfiguration
import org.bukkit.Color
import java.io.File
import java.util.UUID
import kotlin.math.ceil
// ============================================================================
// CONFIGURATION
// ============================================================================
val ENABLED_WORLDS = listOf("world", "world_nether", "world_the_end")
val XP_DECAY_START_MINUTES = 5
val XP_DECAY_PERCENT = 0.5 // Loses 0.5% of remaining XP
val XP_DECAY_INTERVAL_SECONDS = 5
val HEAD_ROTATION_SPEED = 3f // Degrees per tick
val UPDATE_INTERVAL_TICKS = 20 // Update graves every second
val AUTO_EQUIP_ARMOR = true
val DROP_ITEMS_ON_REMOVAL = true
// Hologram configuration
val HOLOGRAM_HEIGHT = 0.75 // Height above armor stand
val HOLOGRAM_LINES = listOf(
"<gold>%player%'s Grave</gold>",
"<gray>Items: <white>%items%</white></gray>",
"<yellow>XP: <white>%xp%</white></yellow>"
)
// ============================================================================
// DATA CLASSES
// ============================================================================
data class Grave(
val uuid: String,
val playerUUID: UUID,
val playerName: String,
val location: Location,
var storedXP: Int,
var spawnTime: Long,
val inventory: Inventory,
val armorStand: ArmorStand,
val holograms: List<ArmorStand>,
var lastXPDecay: Long = System.currentTimeMillis()
) {
fun itemCount(): Int = inventory.contents.count { it != null && it.type != Material.AIR }
fun isEmpty(): Boolean = itemCount() == 0 && storedXP == 0
fun updateHologram() {
// Update each hologram line
val lines = HOLOGRAM_LINES.map { line ->
line.replace("%player%", playerName)
.replace("%items%", itemCount().toString())
.replace("%xp%", storedXP.toString())
}
for ((index, hologram) in holograms.withIndex()) {
if (index < lines.size) {
hologram.customName(MiniMessage.miniMessage().deserialize(lines[index]))
}
}
}
}
data class SerializableGrave(
val uuid: String,
val playerUUID: String,
val playerName: String,
val world: String,
val x: Double,
val y: Double,
val z: Double,
val yaw: Float,
val pitch: Float,
val storedXP: Int,
val spawnTime: Long,
val lastXPDecay: Long,
val items: List<Map<String, Any>>
)
// ============================================================================
// STATE
// ============================================================================
val graves = mutableMapOf<String, Grave>()
// ============================================================================
// PERSISTENCE
// ============================================================================
fun getGravesFile(): File = getDataFile("graves.yml", "graves")
fun saveGraves() {
try {
val config = YamlConfiguration()
for ((uuid, grave) in graves) {
val path = "graves.$uuid"
config.set("$path.playerUUID", grave.playerUUID.toString())
config.set("$path.playerName", grave.playerName)
config.set("$path.world", grave.location.world.name)
config.set("$path.x", grave.location.x)
config.set("$path.y", grave.location.y)
config.set("$path.z", grave.location.z)
config.set("$path.yaw", grave.location.yaw)
config.set("$path.pitch", grave.location.pitch)
config.set("$path.storedXP", grave.storedXP)
config.set("$path.spawnTime", grave.spawnTime)
config.set("$path.lastXPDecay", grave.lastXPDecay)
// Save items
val items = mutableListOf<Map<String, Any>>()
for ((index, item) in grave.inventory.contents.withIndex()) {
if (item != null && item.type != Material.AIR) {
items.add(mapOf(
"slot" to index,
"item" to item.serialize()
))
}
}
config.set("$path.items", items)
}
config.save(getGravesFile())
logInfo("Saved ${graves.size} grave(s)")
} catch (e: Exception) {
logSevere("Failed to save graves: ${e.message}")
}
}
fun loadGraves() {
val file = getGravesFile()
if (!file.exists()) {
logInfo("No graves file found")
return
}
try {
val config = YamlConfiguration.loadConfiguration(file)
val gravesSection = config.getConfigurationSection("graves") ?: return
for (uuid in gravesSection.getKeys(false)) {
val path = "graves.$uuid"
val playerUUID = UUID.fromString(config.getString("$path.playerUUID") ?: continue)
val playerName = config.getString("$path.playerName") ?: "Unknown"
val worldName = config.getString("$path.world") ?: continue
val world = server.getWorld(worldName) ?: continue
val location = Location(
world,
config.getDouble("$path.x"),
config.getDouble("$path.y"),
config.getDouble("$path.z"),
config.getDouble("$path.yaw").toFloat(),
config.getDouble("$path.pitch").toFloat()
)
val storedXP = config.getInt("$path.storedXP", 0)
val spawnTime = config.getLong("$path.spawnTime", System.currentTimeMillis())
val lastXPDecay = config.getLong("$path.lastXPDecay", System.currentTimeMillis())
// Load items
val itemsList = config.getMapList("$path.items")
val items = mutableListOf<Pair<Int, ItemStack>>()
for (itemMap in itemsList) {
@Suppress("UNCHECKED_CAST")
val slot = (itemMap["slot"] as? Number)?.toInt() ?: continue
@Suppress("UNCHECKED_CAST")
val itemData = itemMap["item"] as? Map<String, Any> ?: continue
val item = ItemStack.deserialize(itemData)
items.add(slot to item)
}
// Spawn the grave
val offlinePlayer = server.getOfflinePlayer(playerUUID)
spawnGrave(uuid, offlinePlayer, location, items, storedXP, spawnTime, lastXPDecay)
}
logInfo("Loaded ${graves.size} grave(s)")
} catch (e: Exception) {
logSevere("Failed to load graves: ${e.message}")
e.printStackTrace()
}
}
// ============================================================================
// GRAVE SPAWNING
// ============================================================================
fun createPlayerHead(player: org.bukkit.OfflinePlayer): ItemStack {
val head = ItemStack(Material.PLAYER_HEAD)
val meta = head.itemMeta as SkullMeta
meta.owningPlayer = player
head.itemMeta = meta
return head
}
fun spawnGrave(
uuid: String,
player: org.bukkit.OfflinePlayer,
location: Location,
items: List<Pair<Int, ItemStack>>,
storedXP: Int,
spawnTime: Long = System.currentTimeMillis(),
lastXPDecay: Long = System.currentTimeMillis()
) {
val world = location.world
val playerName = player.name ?: "Unknown"
// Create inventory
val inventory = server.createInventory(
null,
54,
componentFromLegacy("§8${playerName}'s Grave")
)
for ((slot, item) in items) {
if (slot in 0..53) {
inventory.setItem(slot, item)
}
}
// Spawn armor stand with player head (lowered to prevent clipping)
val armorStandLoc = location.clone().add(0.0, -0.5, 0.0)
val armorStand = world.spawn(armorStandLoc, ArmorStand::class.java).apply {
isSmall = true
isInvisible = true
setBasePlate(false)
setArms(false)
setGravity(false)
equipment.helmet = createPlayerHead(player)
isCustomNameVisible = false
isPersistent = true
}
// Spawn hologram (Multiple Armor Stands with custom names)
val holograms = mutableListOf<ArmorStand>()
val lineSpacing = 0.25 // Space between lines
for ((index, line) in HOLOGRAM_LINES.withIndex()) {
val hologramLoc = location.clone().add(0.0, HOLOGRAM_HEIGHT - (index * lineSpacing), 0.0)
val hologram = world.spawn(hologramLoc, ArmorStand::class.java).apply {
isInvisible = true
isMarker = true
setGravity(false)
isCustomNameVisible = true
isPersistent = true
}
holograms.add(hologram)
}
// Create grave object
val grave = Grave(
uuid = uuid,
playerUUID = player.uniqueId,
playerName = playerName,
location = location,
storedXP = storedXP,
spawnTime = spawnTime,
inventory = inventory,
armorStand = armorStand,
holograms = holograms,
lastXPDecay = lastXPDecay
)
grave.updateHologram()
graves[uuid] = grave
logInfo("Spawned grave for $playerName at ${location.blockX}, ${location.blockY}, ${location.blockZ}")
}
fun removeGrave(grave: Grave, dropItems: Boolean = DROP_ITEMS_ON_REMOVAL) {
// Drop items if configured
if (dropItems) {
for (item in grave.inventory.contents) {
if (item != null && item.type != Material.AIR) {
grave.location.world.dropItemNaturally(grave.location, item)
}
}
// Drop XP
if (grave.storedXP > 0) {
val orb = grave.location.world.spawn(
grave.location,
org.bukkit.entity.ExperienceOrb::class.java
)
orb.experience = grave.storedXP
}
}
// Remove entities
grave.armorStand.remove()
grave.holograms.forEach { it.remove() }
// Close any open inventories
for (viewer in grave.inventory.viewers.toList()) {
viewer.closeInventory()
}
graves.remove(grave.uuid)
logInfo("Removed grave for ${grave.playerName}")
}
// ============================================================================
// EVENT HANDLERS
// ============================================================================
on<PlayerDeathEvent> { event ->
val player = event.entity
val world = player.world
// Check if graves are enabled in this world
if (world.name !in ENABLED_WORLDS) return@on
// Check if player has items or XP
val hasItems = event.drops.isNotEmpty()
val hasXP = player.totalExperience > 0
if (!hasItems && !hasXP) return@on
// Get items
val items = event.drops.mapIndexed { index, item -> index to item.clone() }
// Get XP
val xp = getPlayerExperience(player).toInt()
// Clear drops
event.drops.clear()
event.droppedExp = 0
// Spawn grave
val graveUUID = UUID.randomUUID().toString()
val location = player.location.clone()
spawnGrave(graveUUID, player, location, items, xp)
// Notify player
player.sendRichMessage("<yellow>Your items have been stored in a grave at <white>[${location.blockX}, ${location.blockY}, ${location.blockZ}]</white></yellow>")
}
on<PlayerInteractAtEntityEvent> { event ->
val entity = event.rightClicked
// Find grave by armor stand
val grave = graves.values.find { it.armorStand == entity } ?: return@on
event.isCancelled = true
val player = event.player
// Give XP on any interaction
if (grave.storedXP > 0) {
// Spawn experience orbs that fly to the player
val xpPerOrb = (grave.storedXP / 5).coerceAtLeast(1)
val numOrbs = (grave.storedXP / xpPerOrb).coerceAtMost(10)
for (i in 0 until numOrbs) {
val orbLoc = grave.location.clone().add(
(Math.random() - 0.5) * 0.5,
0.5 + Math.random() * 0.5,
(Math.random() - 0.5) * 0.5
)
val orb = player.world.spawn(orbLoc, ExperienceOrb::class.java)
orb.experience = xpPerOrb
}
playSound(player, SOUND_EXPERIENCE_PICKUP)
grave.storedXP = 0
grave.updateHologram()
}
// Shift-click for instant pickup
if (player.isSneaking) {
handleInstantPickup(player, grave)
} else {
// Regular click - open GUI
player.openInventory(grave.inventory)
}
}
on<InventoryCloseEvent> { event ->
// Update hologram when inventory is closed
val grave = graves.values.find { it.inventory == event.inventory } ?: return@on
grave.updateHologram()
// Check if grave is now empty
if (grave.isEmpty()) {
removeGrave(grave, dropItems = false)
}
}
// ============================================================================
// INSTANT PICKUP LOGIC
// ============================================================================
fun handleInstantPickup(player: Player, grave: Grave) {
// Pick up items
var pickedUp = 0
for ((index, item) in grave.inventory.contents.withIndex()) {
if (item == null || item.type == Material.AIR) continue
// Try to auto-equip armor
if (AUTO_EQUIP_ARMOR) {
val equipped = tryEquipArmor(player, item)
if (equipped) {
grave.inventory.setItem(index, null)
pickedUp++
continue
}
}
// Add to inventory
val remaining = player.inventory.addItem(item)
if (remaining.isEmpty()) {
grave.inventory.setItem(index, null)
pickedUp++
} else {
grave.inventory.setItem(index, remaining.values.first())
}
}
if (pickedUp > 0) {
player.sendRichMessage("<green>Picked up <white>$pickedUp</white> item(s) from grave</green>")
}
grave.updateHologram()
// Remove if empty
if (grave.isEmpty()) {
removeGrave(grave, dropItems = false)
}
}
fun tryEquipArmor(player: Player, item: ItemStack): Boolean {
val type = item.type
return when {
(type.name.endsWith("_HELMET") || type == Material.TURTLE_HELMET) && player.inventory.helmet == null -> {
player.inventory.helmet = item
true
}
(type.name.endsWith("_CHESTPLATE") || type == Material.ELYTRA) && player.inventory.chestplate == null -> {
player.inventory.chestplate = item
true
}
type.name.endsWith("_LEGGINGS") && player.inventory.leggings == null -> {
player.inventory.leggings = item
true
}
type.name.endsWith("_BOOTS") && player.inventory.boots == null -> {
player.inventory.boots = item
true
}
else -> false
}
}
// ============================================================================
// UPDATE TASKS
// ============================================================================
var updateTaskId: Int? = null
fun updateGraves() {
val currentTime = System.currentTimeMillis()
val gravesToRemove = mutableListOf<Grave>()
for (grave in graves.values) {
// Rotate armor stand
val currentYaw = grave.armorStand.location.yaw
grave.armorStand.location.yaw = currentYaw + HEAD_ROTATION_SPEED
grave.armorStand.teleport(grave.armorStand.location)
// Handle XP decay
val minutesSinceSpawn = (currentTime - grave.spawnTime) / 1000 / 60
if (minutesSinceSpawn >= XP_DECAY_START_MINUTES && grave.storedXP > 0) {
val timeSinceLastDecay = currentTime - grave.lastXPDecay
val decayIntervalMs = XP_DECAY_INTERVAL_SECONDS * 1000
if (timeSinceLastDecay >= decayIntervalMs) {
// Calculate new XP: lose 5% of remaining
val decayAmount = ceil(grave.storedXP * (XP_DECAY_PERCENT / 100.0)).toInt()
grave.storedXP = (grave.storedXP - decayAmount).coerceAtLeast(0)
grave.lastXPDecay = currentTime
grave.updateHologram()
}
}
// Check if empty
if (grave.isEmpty()) {
gravesToRemove.add(grave)
}
}
// Remove empty graves
for (grave in gravesToRemove) {
removeGrave(grave, dropItems = false)
}
}
// ============================================================================
// COMMANDS
// ============================================================================
command("graves") {
description = "Manage your graves"
usage = "/graves <list|tp> [number]"
permission = "scripts.graves"
aliases = listOf("grave", "mygrave")
execute { sender, args ->
if (sender !is Player) {
sender.sendRichMessage("<red>This command can only be used by players</red>")
return@execute
}
if (args.isEmpty()) {
sender.sendRichMessage("<yellow>Usage: /graves <list|tp> [number]</yellow>")
return@execute
}
when (args[0].lowercase()) {
"list" -> {
val playerGraves = graves.values.filter { it.playerUUID == sender.uniqueId }
.sortedBy { it.spawnTime }
if (playerGraves.isEmpty()) {
sender.sendRichMessage("<yellow>You have no graves</yellow>")
return@execute
}
sender.sendRichMessage("<gold>Your Graves:</gold>")
for ((index, grave) in playerGraves.withIndex()) {
val loc = grave.location
val items = grave.itemCount()
val xp = grave.storedXP
sender.sendRichMessage(
"<gray>${index + 1}. <white>${loc.world.name}</white> " +
"[<white>${loc.blockX}, ${loc.blockY}, ${loc.blockZ}</white>] " +
"- <white>$items</white> items, <yellow>$xp</yellow> XP</gray>"
)
}
}
"tp" -> {
if (args.size < 2) {
sender.sendRichMessage("<red>Usage: /graves tp <number></red>")
return@execute
}
val graveNumber = args[1].toIntOrNull()
if (graveNumber == null || graveNumber < 1) {
sender.sendRichMessage("<red>Invalid grave number</red>")
return@execute
}
val playerGraves = graves.values.filter { it.playerUUID == sender.uniqueId }
.sortedBy { it.spawnTime }
if (graveNumber > playerGraves.size) {
sender.sendRichMessage("<red>Grave #$graveNumber not found</red>")
return@execute
}
val grave = playerGraves[graveNumber - 1]
sender.teleport(grave.location)
sender.sendRichMessage("<green>Teleported to grave #$graveNumber</green>")
}
else -> {
sender.sendRichMessage("<red>Unknown subcommand. Use: list, tp</red>")
}
}
}
tabComplete { sender, args ->
when (args.size) {
1 -> listOf("list", "tp").filter { it.startsWith(args[0].lowercase()) }
2 -> {
if (args[0].lowercase() == "tp" && sender is Player) {
val count = graves.values.count { it.playerUUID == sender.uniqueId }
(1..count).map { it.toString() }
} else {
emptyList()
}
}
else -> emptyList()
}
}
}
command("gravesdebug") {
description = "Debug commands for graves"
usage = "/gravesdebug <info|decay|startdecay> [amount]"
permission = "scripts.graves.admin"
execute { sender, args ->
if (sender !is Player) {
sender.sendRichMessage("<red>This command can only be used by players</red>")
return@execute
}
if (args.isEmpty()) {
sender.sendRichMessage("<yellow>Usage: /gravesdebug <info|decay|startdecay> [amount]</yellow>")
return@execute
}
when (args[0].lowercase()) {
"startdecay" -> {
val playerGraves = graves.values.filter { it.playerUUID == sender.uniqueId }
if (playerGraves.isEmpty()) {
sender.sendRichMessage("<yellow>You have no graves</yellow>")
return@execute
}
for (grave in playerGraves) {
// Set spawn time to be old enough that decay is active
val decayStartTime = System.currentTimeMillis() - (XP_DECAY_START_MINUTES * 60 * 1000)
grave.spawnTime = decayStartTime
grave.lastXPDecay = System.currentTimeMillis()
}
sender.sendRichMessage("<green>Started decay on ${playerGraves.size} grave(s)</green>")
}
"info" -> {
val playerGraves = graves.values.filter { it.playerUUID == sender.uniqueId }
if (playerGraves.isEmpty()) {
sender.sendRichMessage("<yellow>You have no graves</yellow>")
return@execute
}
sender.sendRichMessage("<gold>Your Graves Debug Info:</gold>")
for ((index, grave) in playerGraves.withIndex()) {
val ageMinutes = (System.currentTimeMillis() - grave.spawnTime) / 1000 / 60
val timeSinceDecay = (System.currentTimeMillis() - grave.lastXPDecay) / 1000
val decayActive = ageMinutes >= XP_DECAY_START_MINUTES
sender.sendRichMessage(
"<gray>${index + 1}. Age: <white>${ageMinutes}m</white> | " +
"XP: <yellow>${grave.storedXP}</yellow> | " +
"Decay: <white>${if (decayActive) "ACTIVE" else "INACTIVE"}</white> | " +
"Last decay: <white>${timeSinceDecay}s ago</white></gray>"
)
}
}
"decay" -> {
val amount = args.getOrNull(1)?.toIntOrNull() ?: 1
val playerGraves = graves.values.filter { it.playerUUID == sender.uniqueId }
if (playerGraves.isEmpty()) {
sender.sendRichMessage("<yellow>You have no graves</yellow>")
return@execute
}
var totalDecayed = 0
for (grave in playerGraves) {
if (grave.storedXP > 0) {
repeat(amount) {
val decayAmount = ceil(grave.storedXP * (XP_DECAY_PERCENT / 100.0)).toInt()
grave.storedXP = (grave.storedXP - decayAmount).coerceAtLeast(0)
totalDecayed += decayAmount
}
grave.updateHologram()
}
}
sender.sendRichMessage("<green>Applied $amount decay cycle(s). Total XP lost: <white>$totalDecayed</white></green>")
}
else -> {
sender.sendRichMessage("<red>Unknown subcommand. Use: info, decay, startdecay</red>")
}
}
}
tabComplete { sender, args ->
when (args.size) {
1 -> listOf("info", "decay", "startdecay").filter { it.startsWith(args[0].lowercase()) }
2 -> {
if (args[0].lowercase() == "decay") {
listOf("1", "5", "10", "20")
} else {
emptyList()
}
}
else -> emptyList()
}
}
}
command("gravesadmin") {
description = "Admin commands for graves"
usage = "/gravesadmin <list|remove> [player]"
permission = "scripts.graves.admin"
aliases = listOf("graveadmin")
execute { sender, args ->
if (args.isEmpty()) {
sender.sendRichMessage("<yellow>Usage: /gravesadmin <list|remove> [player]</yellow>")
return@execute
}
when (args[0].lowercase()) {
"list" -> {
if (args.size < 2) {
// List all graves
if (graves.isEmpty()) {
sender.sendRichMessage("<yellow>No graves exist</yellow>")
return@execute
}
sender.sendRichMessage("<gold>All Graves (${graves.size}):</gold>")
for (grave in graves.values.sortedBy { it.spawnTime }) {
val loc = grave.location
sender.sendRichMessage(
"<gray>${grave.playerName} - <white>${loc.world.name}</white> " +
"[<white>${loc.blockX}, ${loc.blockY}, ${loc.blockZ}</white>] " +
"- <white>${grave.itemCount()}</white> items, <yellow>${grave.storedXP}</yellow> XP</gray>"
)
}
} else {
// List specific player's graves
val targetName = args[1]
val target = server.getOfflinePlayer(targetName)
val playerGraves = graves.values.filter { it.playerUUID == target.uniqueId }
.sortedBy { it.spawnTime }
if (playerGraves.isEmpty()) {
sender.sendRichMessage("<yellow>$targetName has no graves</yellow>")
return@execute
}
sender.sendRichMessage("<gold>${targetName}'s Graves:</gold>")
for (grave in playerGraves) {
val loc = grave.location
sender.sendRichMessage(
"<gray><white>${loc.world.name}</white> " +
"[<white>${loc.blockX}, ${loc.blockY}, ${loc.blockZ}</white>] " +
"- <white>${grave.itemCount()}</white> items, <yellow>${grave.storedXP}</yellow> XP</gray>"
)
}
}
}
"removeall" -> {
val count = graves.size
for (grave in graves.values.toList()) {
removeGrave(grave, dropItems = false)
}
sender.sendRichMessage("<green>Removed $count grave(s)</green>")
}
else -> {
sender.sendRichMessage("<red>Unknown subcommand. Use: list, removeall</red>")
}
}
}
tabComplete { sender, args ->
when (args.size) {
1 -> listOf("list", "removeall").filter { it.startsWith(args[0].lowercase()) }
2 -> {
if (args[0].lowercase() == "list") {
server.onlinePlayers.map { it.name }.filter { it.startsWith(args[1], ignoreCase = true) }
} else {
emptyList()
}
}
else -> emptyList()
}
}
}
// ============================================================================
// LIFECYCLE
// ============================================================================
onLoad {
loadGraves()
// Start update task
updateTaskId = server.scheduler.runTaskTimer(
delayTicks = UPDATE_INTERVAL_TICKS.toLong(),
periodTicks = UPDATE_INTERVAL_TICKS.toLong()
) {
updateGraves()
}.taskId
logInfo("Graves system loaded! ${graves.size} grave(s) active")
logInfo("Enabled worlds: ${ENABLED_WORLDS.joinToString(", ")}")
}
onUnload {
// Cancel update task
updateTaskId?.let { server.scheduler.cancelTask(it) }
// Save graves
saveGraves()
// Remove all grave entities
for (grave in graves.values.toList()) {
grave.armorStand.remove()
grave.holograms.forEach { it.remove() }
// Close inventories
for (viewer in grave.inventory.viewers.toList()) {
viewer.closeInventory()
}
}
graves.clear()
logInfo("Graves system unloaded!")
}
// Logging Helpers
// Simple logging functions for Kite scripts
// Each script gets its own logger instance under "Kite/<script-name>"
/**
* Log an informational message
*/
fun logInfo(message: String) = logger.info(message)
/**
* Log a warning message
*/
fun logWarning(message: String) = logger.warn(message)
/**
* Log a severe/error message
*/
fun logSevere(message: String) = logger.error(message)
// Player Helpers
// Functions for manipulating player state (health, XP, etc.)
import org.bukkit.attribute.Attribute
import org.bukkit.entity.Player
import org.bukkit.Location
import org.bukkit.World
import org.bukkit.GameMode
import org.bukkit.Server
/**
* Get a player's maximum health attribute value
*
* @param player The player
* @return Maximum health value (default 20.0)
*/
fun getPlayerMaxHealth(player: Player): Double {
return player.getAttribute(Attribute.MAX_HEALTH)?.value ?: 20.0
}
/**
* Fully heal a player and restore food/saturation
*
* @param player The player to heal
*/
fun healPlayerFully(player: Player) {
player.health = getPlayerMaxHealth(player)
player.foodLevel = 20
player.saturation = 20f
}
/**
* Get a player's total experience points (includes level progress)
* Handles the non-linear XP curve correctly
*
* @param player The player
* @return Total experience points
*/
fun getPlayerExperience(player: Player): Double {
var exp = player.exp.toDouble()
val level = player.level
// Add XP from completed levels
exp += when {
level >= 32 -> (4.5 * level * level - 162.5 * level + 2220)
level >= 17 -> (2.5 * level * level - 40.5 * level + 360)
else -> (level * level + 6.0 * level)
}
return exp
}
/**
* Set a player's total experience points
* Correctly distributes XP into levels and progress
*
* @param player The player
* @param totalExp Total experience points to set
*/
fun setPlayerExperience(player: Player, totalExp: Double) {
var exp = totalExp
player.exp = 0f
player.level = 0
var level = 0
var xpForNextLevel: Double
while (true) {
xpForNextLevel = when {
level >= 32 -> 9.0 * level - 158.0
level >= 17 -> 5.0 * level - 38.0
else -> 2.0 * level + 7.0
}
if (exp < xpForNextLevel) break
exp -= xpForNextLevel
level++
}
player.level = level
player.exp = (exp / xpForNextLevel).toFloat()
}
/**
* Create a location centered in a block (adds 0.5 to x and z coordinates)
* Useful for spawning players in the center of a block
*
* @param world The world
* @param x X coordinate (will have 0.5 added)
* @param y Y coordinate (used as-is)
* @param z Z coordinate (will have 0.5 added)
* @return Centered location
*/
fun createCenteredLocation(world: World, x: Double, y: Double, z: Double): Location {
return Location(world, x + 0.5, y, z + 0.5)
}
/**
* Teleport a player to the main world spawn and set them to survival mode
* Main world is assumed to be server.worlds[0]
*
* @param player The player to teleport
* @param server The server instance
*/
fun teleportToMainWorldSpawn(player: Player, server: Server) {
player.teleport(server.worlds[0].spawnLocation)
player.gameMode = GameMode.SURVIVAL
}
/**
* Safely teleport a player to the main world spawn with fallback handling
* If main world is null, sends a message to the player
*
* @param player The player to teleport
* @param mainWorld The main world (nullable)
* @param fallbackMessage Optional message to send if mainWorld is null
* @return true if teleport succeeded, false if mainWorld was null
*/
fun teleportToMainWorldSpawnSafely(
player: Player,
mainWorld: World?,
fallbackMessage: String = "<red>Main world not found!</red>"
): Boolean {
if (mainWorld != null) {
player.teleport(mainWorld.spawnLocation)
player.gameMode = GameMode.SURVIVAL
return true
} else {
player.sendRichMessage(fallbackMessage)
return false
}
}
/**
* Return a player to the main world spawn from a minigame
* Combines teleport and survival mode setting
*
* @param player The player to return
* @param mainWorld The main world
*/
fun returnPlayerToMainWorld(player: Player, mainWorld: World?) {
if (mainWorld != null) {
player.teleport(mainWorld.spawnLocation)
player.gameMode = GameMode.SURVIVAL
}
}
// Sound Helpers
// Common sounds and playback functions for Kite scripts
import org.bukkit.Location
import org.bukkit.entity.Player
// ============================================================================
// COMMON SOUNDS
// ============================================================================
// These sound names work across recent Minecraft versions
val SOUND_LEVEL_UP = "entity.player.levelup"
val SOUND_CLICK = "ui.button.click"
val SOUND_PLING = "block.note_block.pling"
val SOUND_BASS = "block.note_block.bass"
val SOUND_ANVIL_USE = "block.anvil.use"
val SOUND_TELEPORT = "entity.enderman.teleport"
val SOUND_EXPLODE = "entity.generic.explode"
val SOUND_EXPERIENCE_PICKUP = "entity.experience_orb.pickup"
val SOUND_DRAGON_GROWL = "entity.ender_dragon.growl"
// ============================================================================
// PLAYBACK FUNCTIONS
// ============================================================================
/**
* Play a sound for a specific player at their location
*
* @param player The player who will hear the sound
* @param sound The sound identifier (use SOUND_* constants)
* @param volume Sound volume (default 1.0)
* @param pitch Sound pitch (default 1.0, higher = higher pitch)
*/
fun playSound(player: Player, sound: String, volume: Float = 1f, pitch: Float = 1f) {
player.playSound(player.location, sound, volume, pitch)
}
/**
* Play a sound at a specific location for all nearby players
*
* @param location Where to play the sound
* @param sound The sound identifier (use SOUND_* constants)
* @param volume Sound volume (default 1.0)
* @param pitch Sound pitch (default 1.0, higher = higher pitch)
*/
fun playSoundAt(location: Location, sound: String, volume: Float = 1f, pitch: Float = 1f) {
location.world?.playSound(location, sound, volume, pitch)
}
// Storage Helpers
// Functions for file, YAML, and JSON operations
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
// ============================================================================
// FILE & DIRECTORY HELPERS
// ============================================================================
/**
* Get or create a data directory within the plugin's data folder
*
* @param subDir Subdirectory path (empty for root data folder)
* @return The directory file reference
*/
fun getDataDirectory(subDir: String = ""): File {
val dir = if (subDir.isEmpty()) {
plugin.dataFolder
} else {
File(plugin.dataFolder, subDir)
}
if (!dir.exists()) {
dir.mkdirs()
}
return dir
}
/**
* Get a file reference in the plugin's data folder
*
* @param fileName The file name
* @param subDir Optional subdirectory path
* @return The file reference (may not exist yet)
*/
fun getDataFile(fileName: String, subDir: String = ""): File {
val dir = getDataDirectory(subDir)
return File(dir, fileName)
}
// ============================================================================
// YAML HELPERS
// ============================================================================
/**
* Save a map of data to a YAML file
*
* @param file The file to save to
* @param data Map of key-value pairs to save
*/
fun saveYamlData(file: File, data: Map<String, Any>) {
val config = YamlConfiguration()
for ((key, value) in data) {
config.set(key, value)
}
config.save(file)
}
/**
* Load data from a YAML file into a map
*
* @param file The file to load from
* @return Map of key-value pairs (empty if file doesn't exist)
*/
fun loadYamlData(file: File): Map<String, Any> {
if (!file.exists()) {
return emptyMap()
}
val config = YamlConfiguration.loadConfiguration(file)
return config.getKeys(false).associateWith { config.get(it) ?: "" }
}
// ============================================================================
// SIMPLE JSON HELPERS
// ============================================================================
// These are basic helpers for simple JSON operations without external libraries
// For complex JSON, consider using YAML instead
/**
* Build a JSON object string from key-value pairs
*
* @param pairs Variable number of key-value pairs
* @return JSON object string
*/
fun buildJsonObject(vararg pairs: Pair<String, Any>): String {
val entries = pairs.joinToString(", ") { (key, value) ->
"\"$key\": ${toJsonValue(value)}"
}
return "{$entries}"
}
/**
* Build a JSON array string from values
*
* @param values Variable number of values
* @return JSON array string
*/
fun buildJsonArray(vararg values: Any): String {
val entries = values.joinToString(", ") { toJsonValue(it) }
return "[$entries]"
}
/**
* Convert a Kotlin value to JSON representation
* Internal helper for JSON building
*/
private fun toJsonValue(value: Any): String = when (value) {
is String -> "\"${value.replace("\"", "\\\"")}\""
is Number -> value.toString()
is Boolean -> value.toString()
is Map<*, *> -> buildJsonObject(*value.map { (k, v) -> k.toString() to (v ?: "") }.toTypedArray())
is List<*> -> buildJsonArray(*value.map { it ?: "" }.toTypedArray())
else -> "\"$value\""
}
/**
* Parse a simple JSON object into a map of strings
* NOTE: Very basic parser - use YAML for complex data structures
*
* @param json JSON object string
* @return Map of string key-value pairs
*/
fun parseSimpleJsonObject(json: String): Map<String, String> {
val trimmed = json.trim().removeSurrounding("{", "}")
if (trimmed.isEmpty()) return emptyMap()
val result = mutableMapOf<String, String>()
val parts = trimmed.split(",")
for (part in parts) {
val colonIndex = part.indexOf(':')
if (colonIndex > 0) {
val key = part.substring(0, colonIndex).trim().removeSurrounding("\"")
val value = part.substring(colonIndex + 1).trim().removeSurrounding("\"")
result[key] = value
}
}
return result
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment