diff --git a/.gitignore b/.gitignore index e0d53d8..24b3549 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .gradle .idea build +.zed diff --git a/src/main/kotlin/io/github/beradq/adybubbles/AdyBubbles.kt b/src/main/kotlin/io/github/beradq/adybubbles/AdyBubbles.kt index 2efad84..c6b687d 100644 --- a/src/main/kotlin/io/github/beradq/adybubbles/AdyBubbles.kt +++ b/src/main/kotlin/io/github/beradq/adybubbles/AdyBubbles.kt @@ -6,7 +6,9 @@ import taboolib.common.platform.function.info object AdyBubbles : Plugin() { val adyApi = Adyeshach.api() + val hologramHandler = adyApi.getHologramHandler() + val entityFinder = adyApi.getEntityFinder() override fun onEnable() { info("Successfully running AdyBubbles!") } -} \ No newline at end of file +} diff --git a/src/main/kotlin/io/github/beradq/adybubbles/Bubbles.kt b/src/main/kotlin/io/github/beradq/adybubbles/Bubbles.kt new file mode 100644 index 0000000..8e98b57 --- /dev/null +++ b/src/main/kotlin/io/github/beradq/adybubbles/Bubbles.kt @@ -0,0 +1,27 @@ +package io.github.beradq.adybubbles + +class Bubbles(val items: MutableList, val mode: ChatPopupMode = ChatPopupMode.LOOP) { + val state = BubbleState(0) + + operator fun get(index: Int): String { + return items[index] + } + + class BubbleState( + var index: Int, + ) + + fun next(): String? { + if (items.isEmpty()) return null + val item = items[state.index] + state.index++ + if (state.index >= items.size) { + state.index = 0 + } + return item + } + + fun random(): String { + return items.random() + } +} diff --git a/src/main/kotlin/io/github/beradq/adybubbles/BubblesBundle.kt b/src/main/kotlin/io/github/beradq/adybubbles/BubblesBundle.kt index 1366be1..8699437 100644 --- a/src/main/kotlin/io/github/beradq/adybubbles/BubblesBundle.kt +++ b/src/main/kotlin/io/github/beradq/adybubbles/BubblesBundle.kt @@ -2,8 +2,11 @@ package io.github.beradq.adybubbles import ink.ptms.adyeshach.core.AdyeshachHologram import io.github.beradq.adybubbles.AdyBubbles.adyApi +import io.github.beradq.adybubbles.AdyBubbles.entityFinder +import io.github.beradq.adybubbles.AdyBubbles.hologramHandler import taboolib.common.LifeCycle import taboolib.common.platform.Awake +import taboolib.common.platform.function.submit object BubblesBundle { @@ -12,14 +15,26 @@ object BubblesBundle { return map[key] } - fun popup(npcId: String, text: String) { - val adyEntity = adyApi.getEntityFinder().getEntityFromUniqueId(npcId) ?: error("Entity not found: $npcId") - val hologramList = map.getOrPut(npcId) { mutableListOf() } - val hologram = adyApi.getHologramHandler().createHologram( + fun popup(npcId: String, text: String): AdyeshachHologram { + val adyEntity = entityFinder.getEntityFromUniqueId(npcId) ?: error("Entity not found: $npcId") + val hologramList = map.getOrPut(npcId) { + mutableListOf() + } + val hologram = hologramHandler.createHologram( adyEntity.getLocation().add(0.0, adyEntity.entitySize.height, 0.0), listOf(text) ) hologramList.add(hologram) updateLocation(npcId) + return hologram + } + + fun popupTick(npcId: String, text: String, lifetime: Long) { + val hologram = popup(npcId, text) + val handle = submit(delay = lifetime) { + hologram.remove() + map[text]?.remove(hologram) + this.cancel() + } } fun clear(npcId: String) { @@ -39,7 +54,7 @@ object BubblesBundle { } fun updateLocation(npcId: String) { - val adyEntity = adyApi.getEntityFinder().getEntityFromUniqueId(npcId) ?: error("Entity not found: $npcId") + val adyEntity = entityFinder.getEntityFromUniqueId(npcId) ?: error("Entity not found: $npcId") val hologramList = map.getOrPut(npcId) { mutableListOf() } hologramList.reversed().withIndex().forEach { it.value.teleport( diff --git a/src/main/kotlin/io/github/beradq/adybubbles/BubblesChatCommand.kt b/src/main/kotlin/io/github/beradq/adybubbles/BubblesChatCommand.kt index 64d12ef..f719ace 100644 --- a/src/main/kotlin/io/github/beradq/adybubbles/BubblesChatCommand.kt +++ b/src/main/kotlin/io/github/beradq/adybubbles/BubblesChatCommand.kt @@ -1,39 +1,136 @@ package io.github.beradq.adybubbles +import ink.ptms.adyeshach.taboolib.module.chat.RawMessage import org.bukkit.command.CommandSender import org.bukkit.entity.Player import taboolib.common.platform.command.CommandBody import taboolib.common.platform.command.CommandHeader import taboolib.common.platform.command.subCommand import taboolib.platform.util.nextChatInTick +import io.github.beradq.adybubbles.AdyBubbles.entityFinder +import taboolib.common.platform.ProxyCommandSender +import taboolib.common.platform.ProxyPlayer +import taboolib.common.platform.command.CommandContext +import taboolib.common.platform.function.adaptCommandSender +import taboolib.common.platform.function.adaptPlayer +import taboolib.module.chat.component + +private fun clearMessageScreen(player: ProxyCommandSender) { + for (i in 1..40) player.sendMessage(" ") +} + +fun sendEditMessage(player: ProxyCommandSender, uuid: String, title: String) { + clearMessageScreen(player) + val bubbles = TraitBubblesChat.getBubbles(uuid) ?: Bubbles(mutableListOf(), ChatPopupMode.LOOP) + player.sendMessage("当前正在编辑: $title 的弹出泡泡") + player.sendMessage(" ") + player.sendMessage(" ") + player.sendMessage(" ") + player.sendMessage("弹出模式(点击切换):") + when (bubbles.mode) { + ChatPopupMode.LOOP -> + "[\\[循环\\]](b;u) [\\[随机\\]](cmd=/bubbles-chat editdata $uuid mode random) [\\[单次\\]](cmd=/bubbles-chat editdata $uuid mode once)" + .component().sendTo(player) + + ChatPopupMode.RANDOM -> + "[\\[循环\\]](cmd=/bubbles-chat editdata $uuid mode loop) [\\[随机\\]](b;u) [\\[单次\\]](cmd=/bubbles-chat editdata $uuid mode once)" + .component().sendTo(player) + + ChatPopupMode.ONCE -> + "[\\[循环\\]](cmd=/bubbles-chat editdata $uuid mode loop) [\\[随机\\]](cmd=/bubbles-chat editdata $uuid mode random) [\\[单次\\]](b;u)" + .component().sendTo(player) + } + player.sendMessage(" ") + "[\\[弹出内容\\](点击编辑)](cmd=/bubbles-chat editdata $uuid items)".component().sendTo(player) + player.sendMessage(" ") + player.sendMessage(" ") + player.sendMessage(" ") + player.sendMessage("请展开聊天栏编辑") + player.sendMessage(" ") + player.sendMessage(" ") + player.sendMessage(" ") +} @CommandHeader("bubbles-chat") object BubblesChatCommand { + + @CommandBody + val editdata = subCommand { + dynamic("UUID") { + suggestion(uncheck = true) { _, _ -> + entityFinder.getEntities().map { it.uniqueId } + } + dynamic("FIELD") { + suggestion(uncheck = false) { _, _ -> + listOf("items", "mode") + } + execute { sender, context, _ -> + val field = context["FIELD"] + if (field != "items") { + sender.sendMessage("缺少参数") + return@execute + } + val uuid = context["UUID"] + val npc = entityFinder.getEntityFromUniqueId(uuid) ?: throw Exception("Entity not found") + TraitBubblesChat.edit(sender as Player, npc) + sendEditMessage(adaptCommandSender(sender), uuid, npc.id) + } + dynamic("VALUE") { + execute { sender, context, _ -> + val field = context["FIELD"] + val value = context["VALUE"] + val uuid = context["UUID"] + val npc = entityFinder.getEntityFromUniqueId(uuid) ?: throw Exception("Entity not found") + when (field) { + "mode" -> { + val mode = ChatPopupMode.fromString(value) + TraitBubblesChat.setMode(uuid, mode) + sendEditMessage(adaptCommandSender(sender), uuid, npc.id) + } + + else -> { + sender.sendMessage("Unknown field: $field") + } + } + } + } + } + } + } + @CommandBody val edit = subCommand { - dynamic("NPC_ID") { - suggestion(uncheck = true) { sender, _ -> - AdyBubbles.adyApi.getEntityFinder().getEntities().map { it.id } + dynamic("NPCID") { + suggestion(uncheck = true) { _, _ -> + entityFinder.getEntities().map { it.id } } execute { sender, context, _ -> - val npcId = context["NPC_ID"] - val npcList = AdyBubbles.adyApi.getEntityFinder().getEntitiesFromId(npcId) - if (npcList.size == 1) { - TraitBubblesChat.edit(sender as Player, npcList.first()) + val npcs = entityFinder.getEntitiesFromId(context["NPCID"]) + if (npcs.size == 1) { + val uuid = npcs[0].uniqueId + sendEditMessage(adaptPlayer(sender as Player), uuid, npcs[0].id) } else { - sender.sendMessage("找到了${npcList.size}个NPC:") - npcList.withIndex().forEach { - sender.sendMessage("${it.index + 1}: ${it.value.uniqueId}") + sender.sendMessage("找到了${npcs.size}个NPC:") + for (npc in npcs) { + "[${npc.uniqueId}](cmd=/bubbles-chat edit2 ${npc.uniqueId})".component().sendTo( + adaptCommandSender(sender) + ) } - sender.sendMessage("请通过聊天输入序号指定NPC") - (sender as Player).nextChatInTick(20 * 10, { - val npc = npcList[it.toInt() - 1] - TraitBubblesChat.edit(sender, npc) - }, { - sender.sendMessage("超时") - }) } + } + } + } + @CommandBody + val edit2 = subCommand { + dynamic("UUID") { + suggestion(uncheck = true) { _, _ -> + entityFinder.getEntities().map { it.uniqueId } + } + execute { sender, context, _ -> + val uuid = context["UUID"] + val npc = entityFinder.getEntityFromUniqueId(uuid)!! + sendEditMessage(adaptPlayer(sender as Player), uuid, npc.id) } } } diff --git a/src/main/kotlin/io/github/beradq/adybubbles/BubblesCommand.kt b/src/main/kotlin/io/github/beradq/adybubbles/BubblesCommand.kt index d5e98ac..e9c6ef3 100644 --- a/src/main/kotlin/io/github/beradq/adybubbles/BubblesCommand.kt +++ b/src/main/kotlin/io/github/beradq/adybubbles/BubblesCommand.kt @@ -10,7 +10,7 @@ object BubblesCommand { @CommandBody val popup = subCommand { dynamic("NPC_ID") { - suggestion(uncheck = true) { sender, _ -> + suggestion(uncheck = true) { _, _ -> AdyBubbles.adyApi.getEntityFinder().getEntities().map { it.id } } dynamic("TEXT") { @@ -33,6 +33,29 @@ object BubblesCommand { }) } } + int("LIFE") { + execute { sender, context, _ -> + val npcId = context["NPC_ID"] + val npcList = AdyBubbles.adyApi.getEntityFinder().getEntitiesFromId(npcId) + val text = context["TEXT"] + val lifetime = context.int("LIFE") + if (npcList.size == 1) { + BubblesBundle.popupTick(npcList.first().uniqueId, text, lifetime.toLong()) + } else { + sender.sendMessage("找到了${npcList.size}个NPC:") + npcList.withIndex().forEach { + sender.sendMessage("${it.index + 1}: ${it.value.uniqueId}") + } + sender.sendMessage("请通过聊天输入序号指定NPC") + (sender as Player).nextChatInTick(20 * 10, { + val npcUniqueId = npcList[it.toInt() - 1].uniqueId + BubblesBundle.popupTick(npcUniqueId, text, lifetime.toLong()) + }, { + sender.sendMessage("超时") + }) + } + } + } } } } @@ -41,7 +64,7 @@ object BubblesCommand { @CommandBody val clear = subCommand { dynamic("NPC_ID") { - suggestion(uncheck = true) { sender, _ -> + suggestion(uncheck = true) { _, _ -> AdyBubbles.adyApi.getEntityFinder().getEntities().map { it.id } } execute { sender, context, _ -> diff --git a/src/main/kotlin/io/github/beradq/adybubbles/ChatPopupMode.kt b/src/main/kotlin/io/github/beradq/adybubbles/ChatPopupMode.kt new file mode 100644 index 0000000..2686522 --- /dev/null +++ b/src/main/kotlin/io/github/beradq/adybubbles/ChatPopupMode.kt @@ -0,0 +1,27 @@ +package io.github.beradq.adybubbles + +enum class ChatPopupMode { + LOOP, // 持续循环 + ONCE, // 顺序单次 + RANDOM; // 持续随机 + + override fun toString(): String { + return when (this) { + LOOP -> "loop" + ONCE -> "once" + RANDOM -> "random" + } + } + + companion object { + @JvmStatic + fun fromString(str: String): ChatPopupMode { + return when (str) { + "loop" -> LOOP + "once" -> ONCE + "random" -> RANDOM + else -> throw IllegalArgumentException("Invalid chat popup mode: $str") + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/beradq/adybubbles/TraitBubblesChat.kt b/src/main/kotlin/io/github/beradq/adybubbles/TraitBubblesChat.kt index ef5be8c..366a3c2 100644 --- a/src/main/kotlin/io/github/beradq/adybubbles/TraitBubblesChat.kt +++ b/src/main/kotlin/io/github/beradq/adybubbles/TraitBubblesChat.kt @@ -15,38 +15,60 @@ import taboolib.module.chat.uncolored import java.util.concurrent.CompletableFuture object TraitBubblesChat : Trait() { - private val bubbleLines = mutableMapOf>() - private val bubbleState = mutableMapOf() + private val bubbles = mutableMapOf() private val visibleEntity = mutableListOf() private var handle: PlatformExecutor.PlatformTask? = null + fun getBubbles(uuid: String): Bubbles? { + val conf = data.getConfigurationSection(uuid) ?: return null + val items = conf.getStringList("items") + val mode = conf.getString("mode")?.let { ChatPopupMode.fromString(it) } ?: ChatPopupMode.LOOP + return Bubbles(items.toMutableList(), mode) + } + + fun setBubbles(uuid: String, bubbles: Bubbles) { + data[uuid] = mapOf( + "items" to bubbles.items, + "mode" to bubbles.mode.toString() + ) + uuid pipe this::update + } + override fun edit(player: Player, entityInstance: EntityInstance): CompletableFuture { val future = CompletableFuture() - val content = bubbleLines.getOrPut(entityInstance.uniqueId) { mutableListOf() } + val uuid = entityInstance.uniqueId + val content = bubbles.getOrPut(uuid) { Bubbles(mutableListOf()) }.items player.inputBook(content) { - future.complete(null) + (future::complete.bind(null))() if (it.all { s -> s.isBlank() }) { - data[entityInstance.uniqueId] = null - bubbleState.remove(entityInstance.uniqueId) - bubbleLines.remove(entityInstance.uniqueId) - BubblesBundle.clear(entityInstance.uniqueId) + data[uuid] = null + uuid also bubbles::remove also BubblesBundle::clear } else { - data[entityInstance.uniqueId] = it.uncolored() + if (!data.contains(uuid)) data[uuid] = mapOf("items" to it.uncolored()) + else data.getConfigurationSection(uuid)!!["items"] = it.uncolored() } - update(entityInstance.uniqueId) + uuid pipe this::update } return future } - private fun update(entityInstanceUniqueId: String) { - if (!data.contains(entityInstanceUniqueId)) return - val lines = data.getStringList(entityInstanceUniqueId) - bubbleLines[entityInstanceUniqueId] = lines + fun setMode(uuid: String, mode: ChatPopupMode) { + if (!data.contains(uuid)) data[uuid] = mapOf("mode" to mode.toString()) + else data.getConfigurationSection(uuid)!!["mode"] = mode.toString() + uuid pipe this::update } - private fun fistUpdate(entityInstanceUniqueId: String) { - if (bubbleLines.containsKey(entityInstanceUniqueId)) return - update(entityInstanceUniqueId) + private fun update(uuid: String) { + if (!data.contains(uuid)) return + val innerData = data.getConfigurationSection(uuid)!! + val items = if (innerData.contains("items")) innerData.getStringList("items") else emptyList() + val mode = ChatPopupMode.fromString(innerData.getString("mode") ?: "loop") + bubbles[uuid] = Bubbles(items.toMutableList(), mode) + } + + private fun fistUpdate(uuid: String) { + if (bubbles.containsKey(uuid)) return + update(uuid) } @@ -62,33 +84,29 @@ object TraitBubblesChat : Trait() { } @SubscribeEvent(priority = EventPriority.MONITOR, ignoreCancelled = true) + // 仅在可视时渲染聊天气泡 private fun onVisible(e: AdyeshachEntityVisibleEvent) { if (e.entity.isPublic()) { - fistUpdate(e.entity.uniqueId) + val uuid = e.entity.uniqueId + fistUpdate(uuid) if (e.visible) { - if (visibleEntity.contains(e.entity.uniqueId)) return - visibleEntity.add(e.entity.uniqueId) + if (visibleEntity.contains(uuid)) return + visibleEntity.add(uuid) } else { - visibleEntity.remove(e.entity.uniqueId) - BubblesBundle.clear(e.entity.uniqueId) + uuid also visibleEntity::remove also BubblesBundle::clear } } } - private fun nextBubble(entityInstanceUniqueId: String) { - val bubbleLine = bubbleLines[entityInstanceUniqueId] ?: return - if (bubbleLine.isEmpty()) return - var state = bubbleState[entityInstanceUniqueId] ?: 0 - if (state >= bubbleLine.size) { - state = 0 - } - val currentLine = bubbleLine[state] - BubblesBundle.popup(entityInstanceUniqueId, currentLine) - BubblesBundle.limit(entityInstanceUniqueId, Config.bubblesConfig.chat.limit) - bubbleState[entityInstanceUniqueId] = state + private fun nextBubble(uuid: String) { + val bubble = bubbles[uuid]?.next() ?: return + if (bubble.isEmpty()) return + + BubblesBundle.popup(uuid, bubble) + BubblesBundle.limit(uuid, Config.bubblesConfig.chat.limit) } override fun id(): String { return "bubbles-chat" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/io/github/beradq/adybubbles/Utils.kt b/src/main/kotlin/io/github/beradq/adybubbles/Utils.kt new file mode 100644 index 0000000..905e56b --- /dev/null +++ b/src/main/kotlin/io/github/beradq/adybubbles/Utils.kt @@ -0,0 +1,18 @@ +package io.github.beradq.adybubbles + +inline infix fun T.pipe(func: (T) -> R): R { + return func(this) +} + +infix fun T.pipe(func: Pair<(T) -> R, (T) -> S>): Pair { + return func.first(this) to func.second(this) +} + +inline infix fun T.also(func: (T) -> R): T { + func(this) + return this +} + +infix fun R, T, R> F.bind(value: T): () -> R { + return { this(value) } +}