Last active
December 24, 2025 01:27
-
-
Save NigelThorne/b78dd17387e759ec2b08753d4973467d to your computer and use it in GitHub Desktop.
Grave MInecraft Kite Script Plugin (v1)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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)) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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!") | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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 | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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