From 03137e61152a4529d28857f36bcaa615b3ce24d7 Mon Sep 17 00:00:00 2001
From: BERADQ <adqber123@outlook.com>
Date: Wed, 6 Nov 2024 23:02:11 +0800
Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E8=A7=84=E6=A8=A1=E9=87=8D=E6=9E=84?=
 =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BC=B9=E5=87=BA=E6=9C=BA=E5=88=B6=EF=BC=8C?=
 =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=80=A7=E8=83=BD=E3=80=82=20=E6=96=B0?=
 =?UTF-8?q?=E7=9A=84=E9=85=8D=E7=BD=AE=E9=A1=B9=E7=9B=AE=EF=BC=9A=20rest-d?=
 =?UTF-8?q?elay=E3=80=82=20=E6=97=B6=E9=97=B4=E7=9B=B8=E5=85=B3=E7=9A=86?=
 =?UTF-8?q?=E5=8F=AF=E8=AE=BE=E7=BD=AE=E9=9A=8F=E6=9C=BA=E6=97=B6=E9=97=B4?=
 =?UTF-8?q?=E3=80=82=20=E4=BC=98=E5=8C=96=E6=B8=B8=E6=88=8F=E5=86=85?=
 =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=B3=A1=E6=B3=A1=E7=9A=84=E7=BC=96=E8=BE=91?=
 =?UTF-8?q?=E4=BD=93=E9=AA=8C=E3=80=82=20=E6=97=A0=E7=A0=B4=E5=9D=8F?=
 =?UTF-8?q?=E6=80=A7=E4=BF=AE=E6=94=B9=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md                                     |  40 +++-
 build.gradle.kts                              |  15 +-
 gradle.properties                             |   2 +-
 .../io/github/beradq/adybubbles/AdyBubbles.kt |  25 ++-
 .../io/github/beradq/adybubbles/Bubbles.kt    |  57 +++++-
 .../beradq/adybubbles/BubblesChatCommand.kt   | 173 ++++++++++++++----
 .../io/github/beradq/adybubbles/Config.kt     |   8 +-
 .../beradq/adybubbles/TraitBubblesChat.kt     |  92 +++++-----
 .../io/github/beradq/adybubbles/Utils.kt      |  51 +++++-
 9 files changed, 347 insertions(+), 116 deletions(-)

diff --git a/README.md b/README.md
index 160e6b2..9517a91 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
 # AdyBubbles
 
+版本: 2.5.0-SNAPSHOT
+
 ## 指令
 
 - `bubbles popup <NPC_ID> <TEXT> [LIFE]` - 用于弹出泡泡
@@ -7,13 +9,36 @@
 - `bubbles-chat edit <NPC_ID>` - 用于编辑NPC顺序弹出的泡泡
 - `bubbles-chat play <NPC_UUID>` - 用于播放NPC模式为ONCE弹出的泡泡
 
-## 编辑NPC顺序弹出的泡泡
+## 游戏内编辑NPC的对话泡泡 (推荐)
 
-使用指令 `bubbles-chat edit <NPC_ID>`
+使用指令 bubbles-chat edit <NPC_ID> 并根据提示操作。
 
-根据提示操作
+## 通过文件编辑NPC的对话泡泡 (不推荐)
 
-## 配置
+本插件的对话泡泡以 Adyeshach 的 Trait 形式存在,详见 特征。
+
+Adyeshach 的特征保存为 yaml 存在于 `~/Adyeshach/npc/traits` 下。
+
+本插件的对话泡泡特征文件名为 bubbles-chat。
+
+则需要编辑 `~/Adyeshach/npc/traits/bubbles-chat.yml` 内的内容。
+
+Example:
+```yaml
+44cb049b-c385-4654-b52f-cdc5a09dfb66: # NPC UUID
+  mode: loop # 弹出模式。
+  items: # 对话内容队列,每一个元素为一条。
+    - 你好
+    - 再见
+  period: 60 # 泡泡弹出间隔时间,可以是随机区间(单位:Tick) 可选参数,不填则跟随全局设置
+  rest-delay: 0 # 泡泡弹出每轮后的等待时长(区间同理) 可选参数,不填则跟随全局设置
+  # 模式:
+  # loop 只要NPC在玩家视野内就循环队列播放
+  # random 只要NPC在玩家视野内就一直随机队列播放
+  # once 仅当指令触发时播放一次
+```
+
+## 全局配置
 
 Example:
 
@@ -21,9 +46,12 @@ Example:
 offset: 0.5 # 泡泡的偏移量(向上)
 line-height: 0.5 # 泡泡的间距(向上)
 chat:
-  period: 60 # 泡泡弹出间隔时间
+  period: 60 # 泡泡弹出间隔时间,可以是随机区间(单位:Tick)
+  # period: [60, 100] # 三秒到五秒内随机时长
+  rest-delay: 0 # 泡泡弹出每轮后的等待时长(区间同理)
+  # rest-delay: [0, 100] # 泡泡弹出一轮后等待随机时长后再开启新一轮
   limit: 2 # 泡泡弹出数量限制(超出会自动清楚最早的泡泡)
-  lifetime: 20 # 泡泡存在的时间限制(超出会自动清除泡泡)
+  lifetime: 20 # 泡泡时间限制(超出会自动清除泡泡)
 ```
 
 ## 构建发行版本
diff --git a/build.gradle.kts b/build.gradle.kts
index 5828400..cfebcd0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,5 +1,5 @@
-import io.izzel.taboolib.gradle.BUKKIT
 import io.izzel.taboolib.gradle.BUKKIT_ALL
+import io.izzel.taboolib.gradle.NMS_UTIL
 import io.izzel.taboolib.gradle.UNIVERSAL
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
@@ -12,7 +12,18 @@ plugins {
 taboolib {
     env {
         // 安装模块
-        install(UNIVERSAL, BUKKIT_ALL)
+        install(UNIVERSAL, BUKKIT_ALL, NMS_UTIL)
+    }
+    description {
+        name("AdyBubbles")
+        desc("为Ady实体显示气泡")
+        contributors {
+            name("俗手")
+        }
+        dependencies {
+            name("Adyeshach")
+        }
+        load("POSTWORLD")
     }
     version { taboolib = "6.1.2-beta10" }
 }
diff --git a/gradle.properties b/gradle.properties
index 75e3b35..10003cd 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
 group=io.github.beradq.adybubbles
-version=2.0.0-SNAPSHOT
+version=2.5.0-SNAPSHOT
 kotlin.incremental=true
 kotlin.incremental.java=true
 kotlin.caching.enabled=true
diff --git a/src/main/kotlin/io/github/beradq/adybubbles/AdyBubbles.kt b/src/main/kotlin/io/github/beradq/adybubbles/AdyBubbles.kt
index c6b687d..6b90ce1 100644
--- a/src/main/kotlin/io/github/beradq/adybubbles/AdyBubbles.kt
+++ b/src/main/kotlin/io/github/beradq/adybubbles/AdyBubbles.kt
@@ -1,14 +1,35 @@
 package io.github.beradq.adybubbles
 
 import ink.ptms.adyeshach.core.Adyeshach
+import ink.ptms.adyeshach.core.AdyeshachAPI
 import taboolib.common.platform.Plugin
 import taboolib.common.platform.function.info
 
+//      ___       __      ____        __    __    __
+//     /   | ____/ /_  __/ __ )__  __/ /_  / /_  / /__  _____
+//    / /| |/ __  / / / / __  / / / / __ \/ __ \/ / _ \/ ___/
+//   / ___ / /_/ / /_/ / /_/ / /_/ / /_/ / /_/ / /  __(__  )
+//  /_/  |_\__,_/\__, /_____/\__,_/_.___/_.___/_/\___/____/
+//              /____/
+// 作者: 俗手
+
+
 object AdyBubbles : Plugin() {
-    val adyApi = Adyeshach.api()
+    val adyApi: AdyeshachAPI = Adyeshach.api()
+
     val hologramHandler = adyApi.getHologramHandler()
     val entityFinder = adyApi.getEntityFinder()
     override fun onEnable() {
-        info("Successfully running AdyBubbles!")
+        info("AdyBubbles 已启用!")
+    }
+
+    override fun onActive() {
+        info("\u001B[36m" + "    ___       __      ____        __    __    __         " + "\u001B[0m")
+        info("\u001B[36m" + "   /   | ____/ /_  __/ __ )__  __/ /_  / /_  / /__  _____" + "\u001B[0m")
+        info("\u001B[36m" + "  / /| |/ __  / / / / __  / / / / __ \\/ __ \\/ / _ \\/ ___/" + "\u001B[0m")
+        info("\u001B[36m" + " / ___ / /_/ / /_/ / /_/ / /_/ / /_/ / /_/ / /  __(__  ) " + "\u001B[0m")
+        info("\u001B[36m" + "/_/  |_\\__,_/\\__, /_____/\\__,_/_.___/_.___/_/\\___/____/  " + "\u001B[0m")
+        info("\u001B[36m" + "            /____/                                       " + "\u001B[0m")
+        info("\u001B[34m" + "AdyBubbles" + "\u001B[32m" + " 已加载!" + "\u001B[0m")
     }
 }
diff --git a/src/main/kotlin/io/github/beradq/adybubbles/Bubbles.kt b/src/main/kotlin/io/github/beradq/adybubbles/Bubbles.kt
index 5e9b088..0b2f305 100644
--- a/src/main/kotlin/io/github/beradq/adybubbles/Bubbles.kt
+++ b/src/main/kotlin/io/github/beradq/adybubbles/Bubbles.kt
@@ -1,37 +1,78 @@
 package io.github.beradq.adybubbles
 
-class Bubbles(val items: MutableList<String>, val mode: ChatPopupMode = ChatPopupMode.LOOP) : Cloneable {
+class Bubbles(val items: MutableList<String>, val config: BubbleConfig) : Cloneable {
     private val state = BubbleState(0)
+    private var restDelay = config.restDelayRange.random()
+    private var period = config.period.random()
 
     operator fun get(index: Int): String {
         return items[index]
     }
 
-    class BubbleState(
+    data class BubbleConfig(
+        val mode: ChatPopupMode,
+        val _restDelayRange: LongRange? = null,
+        val _period: LongRange? = null
+    ) {
+        val period: LongRange get() = _period ?: Config.bubblesConfig.chat.period
+        val restDelayRange: LongRange get() = _restDelayRange ?: Config.bubblesConfig.chat.restDelay
+    }
+
+    data class BubbleState(
         var index: Int,
+        var timeCount: Long = 0
     ) : Cloneable
 
     fun next(): String? {
+        if (config.mode == ChatPopupMode.RANDOM) return items.random()
         if (items.isEmpty()) return null
         val item = items[state.index]
         state.index++
-        if (state.index >= items.size && mode == ChatPopupMode.LOOP) {
+        if (state.index >= items.size && config.mode == ChatPopupMode.LOOP) {
             state.index = 0
         }
         return item
     }
 
-    fun resetState() {
-        state.index = 0
+    fun tick(uuid: String) {
+        if (state.timeCount == -1L) return
+        state.timeCount += 1
+        if (state.timeCount == period) {
+            popup(uuid, next() ?: return)
+        }
+        if (state.timeCount >= period) {
+            if (state.index != 0 || config.mode != ChatPopupMode.LOOP) {
+                state.timeCount = if (config.mode == ChatPopupMode.ONCE) -1 else 0
+                period = config.period.random()
+            }
+            if (state.timeCount >= period + restDelay) {
+                state.timeCount = 0
+                period = config.period.random()
+                restDelay = config.restDelayRange.random()
+            }
+        }
     }
 
-    fun random(): String {
-        return items.random()
+    private fun popup(uuid: String, text: String) {
+        if (Config.bubblesConfig.chat.lifetime != null) {
+            BubblesBundle.popupTick(uuid, text, Config.bubblesConfig.chat.lifetime!!)
+        } else {
+            BubblesBundle.popup(uuid, text)
+        }
+        BubblesBundle.limit(uuid, Config.bubblesConfig.chat.limit)
+    }
+
+    fun resetState() {
+        state.index = 0
+        state.timeCount = 0
+        restDelay = config.restDelayRange.random()
+        period = config.period.random()
     }
 
     public override fun clone(): Bubbles {
-        val bubbles = Bubbles(items.toMutableList(), mode)
+        val bubbles = Bubbles(items.toMutableList(), config.copy())
         bubbles.state.index = state.index
+        bubbles.state.timeCount = state.timeCount
         return bubbles
     }
 }
diff --git a/src/main/kotlin/io/github/beradq/adybubbles/BubblesChatCommand.kt b/src/main/kotlin/io/github/beradq/adybubbles/BubblesChatCommand.kt
index 44b61f7..4f45aec 100644
--- a/src/main/kotlin/io/github/beradq/adybubbles/BubblesChatCommand.kt
+++ b/src/main/kotlin/io/github/beradq/adybubbles/BubblesChatCommand.kt
@@ -7,44 +7,94 @@ import taboolib.common.platform.command.CommandHeader
 import taboolib.common.platform.command.subCommand
 import io.github.beradq.adybubbles.AdyBubbles.entityFinder
 import taboolib.common.platform.ProxyCommandSender
+import taboolib.common.platform.command.bool
 import taboolib.common.platform.function.adaptCommandSender
 import taboolib.common.platform.function.adaptPlayer
 import taboolib.module.chat.component
+import taboolib.module.nms.*
 
 private fun clearMessageScreen(player: ProxyCommandSender) {
-    for (i in 1..40) player.sendMessage(" ")
+    for (i in 1..5) player.sendMessage(" ")
 }
 
-fun sendEditMessage(player: ProxyCommandSender, uuid: String, title: String) {
+private fun edd(uuid: String, cmd: String): String {
+    return "/bubbles-chat editdata true $uuid $cmd"
+}
+
+private fun String.replaceBracketForSafety(): String {
+    return this.replace("[", "\\[").replace("]", "\\]")
+}
+
+private fun sendEditMessage(player: ProxyCommandSender, uuid: String, title: String) {
     clearMessageScreen(player)
-    val bubbles = TraitBubblesChat.getBubbles(uuid) ?: Bubbles(mutableListOf(), ChatPopupMode.LOOP)
+    val bubbles = TraitBubblesChat.getOrCreateBubbles(uuid)
     player.sendMessage("当前正在编辑: $title 的弹出泡泡")
     player.sendMessage(" ")
     player.sendMessage(" ")
     player.sendMessage(" ")
-    player.sendMessage("弹出模式(点击切换):")
-    when (bubbles.mode) {
+    "[§b\\[ 弹出内容 \\]§f](cmd=${edd(uuid, "items")})".component().sendTo(player)
+    player.sendMessage(" ")
+    player.sendMessage(" ")
+    player.sendMessage("弹出模式:")
+    when (bubbles.config.mode) {
         ChatPopupMode.LOOP ->
-            "[\\[循环\\]](b;u) [\\[随机\\]](cmd=/bubbles-chat editdata $uuid mode random) [\\[单次\\]](cmd=/bubbles-chat editdata $uuid mode once)"
+            "[§a\\[循环\\]§f](b;u) [§b\\[随机\\]§f](cmd=${edd(uuid, "mode random")}) [§b\\[单次\\]§f](cmd=${
+                edd(
+                    uuid,
+                    "mode once"
+                )
+            })"
                 .component().sendTo(player)
 
         ChatPopupMode.RANDOM ->
-            "[\\[循环\\]](cmd=/bubbles-chat editdata $uuid mode loop) [\\[随机\\]](b;u) [\\[单次\\]](cmd=/bubbles-chat editdata $uuid mode once)"
+            "[§b\\[循环\\]§f](cmd=${edd(uuid, "mode loop")}) [§a\\[随机\\]§f](b;u) [§b\\[单次\\]§f](cmd=${
+                edd(
+                    uuid,
+                    "mode once"
+                )
+            })"
                 .component().sendTo(player)
 
         ChatPopupMode.ONCE ->
-            "[\\[循环\\]](cmd=/bubbles-chat editdata $uuid mode loop) [\\[随机\\]](cmd=/bubbles-chat editdata $uuid mode random) [\\[单次\\]](b;u)"
+            "[§b\\[循环\\]§f](cmd=${edd(uuid, "mode loop")}) [§b\\[随机\\]§f](cmd=${
+                edd(
+                    uuid,
+                    "mode random"
+                )
+            }) [§a\\[单次\\]§f](b;u)"
                 .component().sendTo(player)
     }
     player.sendMessage(" ")
-    "[\\[弹出内容\\](点击编辑)](cmd=/bubbles-chat editdata $uuid items)".component().sendTo(player)
+    player.sendMessage(" ")
+    if (bubbles.config._period != null) {
+        "弹出间隔: [§b\\[ ${bubbles.config._period.toMString().replaceBracketForSafety()} \\]§f](cmd=${
+            edd(
+                uuid,
+                "period"
+            )
+        })"
+            .component().sendTo(player)
+    } else {
+        "弹出间隔: [§b\\[ 默认值 \\]§f](cmd=${edd(uuid, "period")})"
+            .component().sendTo(player)
+    }
+    if (bubbles.config._restDelayRange != null) {
+        "每轮间隔: [§b\\[ ${bubbles.config._restDelayRange.toMString().replaceBracketForSafety()} \\]§f](cmd=${
+            edd(
+                uuid,
+                "rest-delay"
+            )
+        })"
+            .component().sendTo(player)
+    } else {
+        "每轮间隔: [§b\\[ 默认值 \\]§f](cmd=${edd(uuid, "rest-delay")})"
+            .component().sendTo(player)
+    }
     player.sendMessage(" ")
     player.sendMessage(" ")
     player.sendMessage(" ")
+    player.sendMessage("蓝色选项可点击编辑")
     player.sendMessage("请展开聊天栏编辑")
-    player.sendMessage(" ")
-    player.sendMessage(" ")
-    player.sendMessage(" ")
 }
 
 @CommandHeader("bubbles-chat")
@@ -68,40 +118,89 @@ object BubblesChatCommand {
     @CommandBody
     @Suppress("unused")
     val editdata = subCommand {
-        dynamic("UUID") {
-            suggestion<CommandSender>(uncheck = true) { _, _ ->
-                entityFinder.getEntities().map { it.uniqueId }
-            }
-            dynamic("FIELD") {
-                suggestion<CommandSender>(uncheck = false) { _, _ ->
-                    listOf("items", "mode")
+        bool("RECALL") {
+            dynamic("UUID") {
+                suggestion<CommandSender>(uncheck = true) { _, _ ->
+                    entityFinder.getEntities().map { it.uniqueId }
                 }
-                execute<CommandSender> { sender, context, _ ->
-                    val field = context["FIELD"]
-                    if (field != "items") {
-                        sender.sendMessage("缺少参数")
-                        return@execute
+                dynamic("FIELD") {
+                    suggestion<CommandSender>(uncheck = false) { _, _ ->
+                        listOf("items", "mode", "period", "rest-delay")
                     }
-                    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<CommandSender> { sender, context, _ ->
                         val field = context["FIELD"]
-                        val value = context["VALUE"]
+                        val recall = context.bool("RECALL")
                         val uuid = context["UUID"]
-                        val npc = entityFinder.getEntityFromUniqueId(uuid) ?: throw Exception("Entity not found")
+                        val bubbles = TraitBubblesChat.getOrCreateBubbles(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)
+                            "items" -> {
+                                TraitBubblesChat.edit(sender as Player, npc)
+                                sender.sendTitle("请编辑书", null, 1, 20, 1)
+                            }
+
+                            "period" -> {
+                                (sender as Player).inputSign(
+                                    arrayOf(
+                                        bubbles.config._period?.toMString() ?: "",
+                                        "请在第一行输入数字或",
+                                        "区间代表范围内随机"
+                                    )
+                                ) {
+                                    val content = it.first().trim()
+                                    if (content.isEmpty()) {
+                                        TraitBubblesChat.setPeriod(uuid, null)
+                                    } else {
+                                        val range = content.toLongRange()
+                                        TraitBubblesChat.setPeriod(uuid, range)
+                                    }
+                                    if (recall) sendEditMessage(adaptCommandSender(sender), uuid, npc.id)
+                                }
+                            }
+
+                            "rest-delay" -> {
+                                (sender as Player).inputSign(
+                                    arrayOf(
+                                        bubbles.config._restDelayRange?.toMString() ?: "",
+                                        "请在第一行输入数字或",
+                                        "区间代表范围内随机"
+                                    )
+                                ) {
+                                    val content = it.first().trim()
+                                    if (content.isEmpty()) {
+                                        TraitBubblesChat.setRestDelay(uuid, null)
+                                    } else {
+                                        val range = content.toLongRange()
+                                        TraitBubblesChat.setRestDelay(uuid, range)
+                                    }
+                                    if (recall) sendEditMessage(adaptCommandSender(sender), uuid, npc.id)
+                                }
                             }
 
                             else -> {
-                                sender.sendMessage("Unknown field: $field")
+                                sender.sendMessage("缺少参数")
+                                return@execute
+                            }
+                        }
+                    }
+                    dynamic("VALUE") {
+                        execute<CommandSender> { sender, context, _ ->
+                            val field = context["FIELD"]
+                            val recall = context.bool("RECALL")
+                            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)
+                                    if (recall) sendEditMessage(adaptCommandSender(sender), uuid, npc.id)
+                                }
+
+                                else -> {
+                                    sender.sendMessage("Unknown field: $field")
+                                }
                             }
                         }
                     }
diff --git a/src/main/kotlin/io/github/beradq/adybubbles/Config.kt b/src/main/kotlin/io/github/beradq/adybubbles/Config.kt
index 483a007..46a4ab3 100644
--- a/src/main/kotlin/io/github/beradq/adybubbles/Config.kt
+++ b/src/main/kotlin/io/github/beradq/adybubbles/Config.kt
@@ -11,7 +11,8 @@ object Config {
     lateinit var bubblesConfig: BubblesConfig
 
     class BubblesChatConfig(
-        val period: Long,
+        val period: LongRange,
+        val restDelay: LongRange,
         val limit: Int,
         val lifetime: Long?
     )
@@ -28,9 +29,10 @@ object Config {
         val offset = configFile["offset"] as? Double ?: 0.5
         val lineHeight = configFile["line-height"] as? Double ?: 0.5
         val chat = configFile.getConfigurationSection("chat")
-        val period = chat?.get("period") as? Long ?: (20 * 3)
+        val period = chat?.getString("period")?.toLongRange() ?: (20L * 3)..(20 * 3)
         val limit = chat?.get("limit") as? Int ?: 3
+        val restDelayRange = chat?.getString("rest-delay")?.toLongRange() ?: 0L..0
         val lifetime = chat?.get("lifetime") as? Long
-        bubblesConfig = BubblesConfig(offset, lineHeight, BubblesChatConfig(period, limit, lifetime))
+        bubblesConfig = BubblesConfig(offset, lineHeight, BubblesChatConfig(period, restDelayRange, limit, lifetime))
     }
 }
\ 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 569e3f4..a975fe0 100644
--- a/src/main/kotlin/io/github/beradq/adybubbles/TraitBubblesChat.kt
+++ b/src/main/kotlin/io/github/beradq/adybubbles/TraitBubblesChat.kt
@@ -4,49 +4,55 @@ import ink.ptms.adyeshach.core.entity.EntityInstance
 import ink.ptms.adyeshach.core.event.AdyeshachEntityVisibleEvent
 import ink.ptms.adyeshach.impl.entity.trait.Trait
 import ink.ptms.adyeshach.impl.util.Inputs.inputBook
-import taboolib.common.LifeCycle
 import org.bukkit.entity.Player
-import taboolib.common.platform.Awake
+import taboolib.common.platform.Schedule
 import taboolib.common.platform.event.EventPriority
 import taboolib.common.platform.event.SubscribeEvent
-import taboolib.common.platform.function.submit
-import taboolib.common.platform.service.PlatformExecutor
 import taboolib.module.chat.uncolored
 import java.util.concurrent.CompletableFuture
 
 object TraitBubblesChat : Trait() {
     private val bubbles = mutableMapOf<String, Bubbles>()
     private val visibleEntity = mutableListOf<String>()
-    private var handle: PlatformExecutor.PlatformTask? = null
 
     fun getBubbles(uuid: String): Bubbles? {
-        val conf = data.getConfigurationSection(uuid) ?: return null
+        if (!data.contains(uuid)) return null
+        val conf = data.getConfigurationSection(uuid)!!
         val items = conf.getStringList("items")
         val mode = conf.getString("mode")?.let { ChatPopupMode.fromString(it) } ?: ChatPopupMode.LOOP
-        return Bubbles(items.toMutableList(), mode)
+        val period = conf.getString("period")?.toLongRange()
+        val restDelay = conf.getString("rest-delay")?.toLongRange()
+        return Bubbles(items.toMutableList(), Bubbles.BubbleConfig(mode, restDelay, period))
     }
 
     @Suppress("unused")
     fun setBubbles(uuid: String, bubbles: Bubbles) {
         data[uuid] = mapOf(
             "items" to bubbles.items,
-            "mode" to bubbles.mode.toString()
+            "mode" to bubbles.config.mode.toString(),
+            "period" to bubbles.config._period?.toMString(),
+            "rest-delay" to bubbles.config._restDelayRange?.toMString()
         )
         uuid pipe this::update
     }
 
+    fun getOrCreateBubbles(uuid: String): Bubbles {
+        val bubbles = bubbles.getOrPut(uuid) { Bubbles(mutableListOf(), Bubbles.BubbleConfig(ChatPopupMode.LOOP)) }
+        update(uuid)
+        return bubbles
+    }
+
     override fun edit(player: Player, entityInstance: EntityInstance): CompletableFuture<Void> {
         val future = CompletableFuture<Void>()
         val uuid = entityInstance.uniqueId
-        val content = bubbles.getOrPut(uuid) { Bubbles(mutableListOf()) }.items
+        val content = getOrCreateBubbles(uuid).items
         player.inputBook(content) {
-            (future::complete.bind(null))()
+            future.complete(null)
             if (it.all { s -> s.isBlank() }) {
                 data[uuid] = null
                 uuid also bubbles::remove also BubblesBundle::clear
             } else {
-                if (!data.contains(uuid)) data[uuid] = mapOf("items" to it.uncolored())
-                else data.getConfigurationSection(uuid)!!["items"] = it.uncolored()
+                getConfiguration(uuid)["items"] = it.uncolored()
             }
             uuid pipe this::update
         }
@@ -54,17 +60,27 @@ object TraitBubblesChat : Trait() {
     }
 
     fun setMode(uuid: String, mode: ChatPopupMode) {
-        if (!data.contains(uuid)) data[uuid] = mapOf("mode" to mode.toString())
-        else data.getConfigurationSection(uuid)!!["mode"] = mode.toString()
+        getConfiguration(uuid)["mode"] = mode.toString()
         uuid pipe this::update
     }
 
+    fun setPeriod(uuid: String, period: LongRange?) {
+        getConfiguration(uuid)["period"] = period?.toMString()
+        uuid pipe this::update
+    }
+
+    fun setRestDelay(uuid: String, restDelay: LongRange?) {
+        getConfiguration(uuid)["rest-delay"] = restDelay?.toMString()
+        uuid pipe this::update
+    }
+
+    fun getConfiguration(uuid: String): ink.ptms.adyeshach.taboolib.library.configuration.ConfigurationSection {
+        if (!data.contains(uuid)) data[uuid] = mapOf<String, String>()
+        return data.getConfigurationSection(uuid)!!
+    }
+
     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)
+        bubbles[uuid] = getBubbles(uuid) ?: return
     }
 
     private fun fistUpdate(uuid: String) {
@@ -73,44 +89,18 @@ object TraitBubblesChat : Trait() {
     }
 
 
-    @Awake(LifeCycle.ACTIVE)
-    private fun startTicking() {
-        handle?.cancel()
-        handle = submit(period = Config.bubblesConfig.chat.period, async = true) {
-            val ve = visibleEntity.toList()
-            ve.forEach {
-                val bubble = bubbles[it] ?: return@forEach
-                val item = when (bubble.mode) {
-                    ChatPopupMode.LOOP -> {
-                        bubble.next()
-                    }
-
-                    ChatPopupMode.RANDOM -> bubble.random()
-                    else -> return@forEach
-                } ?: return@forEach
-                popup(it, item)
-            }
+    @Schedule(async = true, period = 1)
+    fun tick() {
+        for (uuid in visibleEntity.toList()) {
+            val bubble = bubbles[uuid] ?: continue
+            bubble.tick(uuid)
         }
     }
 
     fun startOnce(uuid: String) {
         val bubble = bubbles[uuid] ?: return
-        if (bubble.mode != ChatPopupMode.ONCE) return
+        if (bubble.config.mode != ChatPopupMode.ONCE) return
         bubble.resetState()
-        val cloneBubble = bubble.clone()
-        submit(period = Config.bubblesConfig.chat.period, async = true) {
-            val item = cloneBubble.next() ?: return@submit this.cancel()
-            popup(uuid, item)
-        }
-    }
-
-    fun popup(uuid: String, text: String) {
-        if (Config.bubblesConfig.chat.lifetime != null) {
-            BubblesBundle.popupTick(uuid, text, Config.bubblesConfig.chat.lifetime!!)
-        } else {
-            BubblesBundle.popup(uuid, text)
-        }
-        BubblesBundle.limit(uuid, Config.bubblesConfig.chat.limit)
     }
 
     @SubscribeEvent(priority = EventPriority.MONITOR, ignoreCancelled = true)
diff --git a/src/main/kotlin/io/github/beradq/adybubbles/Utils.kt b/src/main/kotlin/io/github/beradq/adybubbles/Utils.kt
index 905e56b..571f587 100644
--- a/src/main/kotlin/io/github/beradq/adybubbles/Utils.kt
+++ b/src/main/kotlin/io/github/beradq/adybubbles/Utils.kt
@@ -4,15 +4,54 @@ inline infix fun <T, R> T.pipe(func: (T) -> R): R {
     return func(this)
 }
 
-infix fun <T, R, S> T.pipe(func: Pair<(T) -> R, (T) -> S>): Pair<R, S> {
-    return func.first(this) to func.second(this)
-}
-
 inline infix fun <T, R> T.also(func: (T) -> R): T {
     func(this)
     return this
 }
 
-infix fun <F : (T) -> R, T, R> F.bind(value: T): () -> R {
-    return { this(value) }
+fun parseLongRange(range: String): LongRange? {
+    val rangeStr = range.trim()
+    val start: Long
+    val end: Long
+
+    val startInclusive: Boolean = when (rangeStr[0]) {
+        '[' -> true
+        '(' -> false
+        else -> {
+            // 如果只有一个数字,则返回该数字
+            return rangeStr.toLongOrNull()?.let { it..it }
+        }
+    }
+
+    val endInclusive: Boolean = when (rangeStr[rangeStr.length - 1]) {
+        ']' -> true
+        ')' -> false
+        else -> return null
+    }
+
+    val rangeParts = rangeStr.substring(1, rangeStr.length - 1).split(',').map { it.trim() }
+    if (rangeParts.size != 2) {
+        return null
+    }
+
+    start = rangeParts[0].toLongOrNull() ?: return null
+    end = rangeParts[1].toLongOrNull() ?: return null
+
+    return when {
+        startInclusive && endInclusive -> start..end
+        startInclusive && !endInclusive -> start until end
+        !startInclusive && endInclusive -> (start + 1)..end
+        else -> (start + 1) until end
+    }
 }
+
+fun String.toLongRange() = parseLongRange(this)
+
+fun longRangeToString(range: LongRange): String {
+    val start = range.first
+    val end = range.last
+    if (start == end) return "$start"
+    return "[$start, $end]"
+}
+
+fun LongRange.toMString() = longRangeToString(this)
\ No newline at end of file