Skip to content

Commit e2c8d65

Browse files
CopilotTrilleo
andauthored
Feature: Complete CountdownHelper util with DisplayLocation and message placeholders
Agent-Logs-Url: https://github.com/Trilleo/PaperExamplePlugin/sessions/14d2063c-1ba4-43a4-8216-aae34af88dd1 Co-authored-by: Trilleo <133519132+Trilleo@users.noreply.github.com>
1 parent a25738d commit e2c8d65

2 files changed

Lines changed: 173 additions & 5 deletions

File tree

Lines changed: 153 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,170 @@
11
package com.example.exampleplugin.utils
22

3+
import net.kyori.adventure.bossbar.BossBar
34
import net.kyori.adventure.sound.Sound
45
import net.kyori.adventure.text.minimessage.MiniMessage
6+
import net.kyori.adventure.title.Title
57
import org.bukkit.entity.Player
68
import org.bukkit.plugin.java.JavaPlugin
9+
import java.time.Duration
710

11+
/**
12+
* Helper that runs a per-player countdown and displays the progress through
13+
* one of the available [DisplayLocation]s.
14+
*
15+
* ### Message placeholders
16+
*
17+
* The [start] `message` parameter is a MiniMessage string that may contain the
18+
* following placeholders, which are replaced on every tick:
19+
*
20+
* | Placeholder | Output example |
21+
* |:-------------|:----------------------------------------|
22+
* | `{seconds}` | `5s` (raw remaining seconds) |
23+
* | `{time}` | `1h 2m 3s` (human-readable time string) |
24+
*
25+
* Either or both placeholders may be omitted from the message string.
26+
*
27+
* ### Optional parameters
28+
*
29+
* Pass `null` for [sound] or [finishSound] to play no sound at that point.
30+
* Set [displayLocation] to [DisplayLocation.NONE] to suppress the message
31+
* entirely.
32+
*
33+
* ### Example usage
34+
* ```kotlin
35+
* CountdownHelper().start(
36+
* plugin = plugin,
37+
* player = player,
38+
* seconds = 10,
39+
* message = "<yellow>Starting in <bold>{seconds}</bold> (<gray>{time}</gray>)",
40+
* displayLocation = DisplayLocation.ACTION_BAR,
41+
* sound = Sound.sound(Key.key("minecraft:ui.button.click"), Sound.Source.MASTER, 1f, 1f),
42+
* finishSound = Sound.sound(Key.key("minecraft:entity.player.levelup"), Sound.Source.MASTER, 1f, 1f),
43+
* onFinish = { p -> p.sendMessage("<green>Go!") }
44+
* )
45+
* ```
46+
*/
847
class CountdownHelper {
948
private val mm = MiniMessage.miniMessage()
1049

50+
/**
51+
* Starts a countdown for [player] and ticks every second until [seconds]
52+
* reaches zero.
53+
*
54+
* @param plugin the owning plugin (used to schedule tasks)
55+
* @param player the player to target
56+
* @param seconds total number of seconds to count down from
57+
* @param message MiniMessage string shown each tick; supports
58+
* `{seconds}` and `{time}` placeholders
59+
* @param displayLocation where the message is shown; use
60+
* [DisplayLocation.NONE] to suppress output
61+
* @param sound sound played on every tick, or `null` for silence
62+
* @param finishSound sound played when the countdown ends, or `null`
63+
* for silence
64+
* @param onFinish callback invoked with [player] when the countdown
65+
* reaches zero
66+
*/
1167
fun start(
1268
plugin: JavaPlugin,
1369
player: Player,
1470
seconds: Int,
15-
message: (remaining: Int) -> String,
16-
sound: Sound,
17-
finishSound: Sound,
71+
message: String,
72+
displayLocation: DisplayLocation,
73+
sound: Sound?,
74+
finishSound: Sound?,
1875
onFinish: (Player) -> Unit
1976
) {
20-
// Countdown Logic
77+
require(seconds > 0) { "seconds must be greater than 0" }
78+
79+
var remaining = seconds
80+
81+
val bossBar: BossBar? = if (displayLocation == DisplayLocation.BOSS_BAR) {
82+
BossBar.bossBar(
83+
mm.deserialize(applyPlaceholders(message, remaining, seconds)),
84+
1.0f,
85+
BossBar.Color.BLUE,
86+
BossBar.Overlay.PROGRESS
87+
).also { player.showBossBar(it) }
88+
} else null
89+
90+
var task: org.bukkit.scheduler.BukkitTask? = null
91+
task = plugin.server.scheduler.runTaskTimer(plugin, Runnable {
92+
if (!player.isOnline) {
93+
bossBar?.let { player.hideBossBar(it) }
94+
task?.cancel()
95+
return@Runnable
96+
}
97+
98+
if (remaining <= 0) {
99+
bossBar?.let { player.hideBossBar(it) }
100+
finishSound?.let { player.playSound(it) }
101+
task?.cancel()
102+
onFinish(player)
103+
return@Runnable
104+
}
105+
106+
val formatted = applyPlaceholders(message, remaining, seconds)
107+
108+
when (displayLocation) {
109+
DisplayLocation.NONE -> {}
110+
DisplayLocation.CHAT -> player.sendMessage(mm.deserialize(formatted))
111+
DisplayLocation.TITLE -> player.showTitle(
112+
Title.title(
113+
mm.deserialize(formatted),
114+
mm.deserialize(""),
115+
Title.Times.times(
116+
Duration.ZERO,
117+
Duration.ofMillis(1200),
118+
Duration.ZERO
119+
)
120+
)
121+
)
122+
DisplayLocation.BOSS_BAR -> bossBar?.let {
123+
it.name(mm.deserialize(formatted))
124+
it.progress(remaining.toFloat() / seconds.toFloat())
125+
}
126+
DisplayLocation.ACTION_BAR -> player.sendActionBar(mm.deserialize(formatted))
127+
}
128+
129+
sound?.let { player.playSound(it) }
130+
remaining--
131+
}, 0L, 20L)
132+
}
133+
134+
/**
135+
* Replaces `{seconds}` and `{time}` placeholders in [message].
136+
*
137+
* @param message the raw MiniMessage template
138+
* @param remaining seconds still remaining
139+
* @param total the original total seconds (unused; reserved for future use)
140+
* @return the message with placeholders substituted
141+
*/
142+
private fun applyPlaceholders(message: String, remaining: Int, @Suppress("UNUSED_PARAMETER") total: Int): String {
143+
return message
144+
.replace("{seconds}", "${remaining}s")
145+
.replace("{time}", formatTime(remaining))
146+
}
147+
148+
/**
149+
* Formats a raw second count into a human-readable string.
150+
*
151+
* Examples:
152+
* - `5` → `5s`
153+
* - `90` → `1m 30s`
154+
* - `3665` → `1h 1m 5s`
155+
*
156+
* @param seconds the number of seconds to format (must be ≥ 0)
157+
* @return formatted time string
158+
*/
159+
private fun formatTime(seconds: Int): String {
160+
val hours = seconds / 3600
161+
val minutes = (seconds % 3600) / 60
162+
val secs = seconds % 60
163+
164+
return buildString {
165+
if (hours > 0) append("${hours}h ")
166+
if (minutes > 0) append("${minutes}m ")
167+
append("${secs}s")
168+
}.trim()
21169
}
22-
}
170+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.example.exampleplugin.utils
2+
3+
/**
4+
* The location where countdown messages are displayed to the player.
5+
*
6+
* | Value | Behaviour |
7+
* |:-------------|:-----------------------------------------------------|
8+
* | [NONE] | No message is displayed |
9+
* | [CHAT] | Message is sent to the player's chat |
10+
* | [TITLE] | Message is shown as a screen title |
11+
* | [BOSS_BAR] | Message is shown in a boss bar that depletes over time |
12+
* | [ACTION_BAR] | Message is shown above the hotbar |
13+
*/
14+
enum class DisplayLocation {
15+
NONE,
16+
CHAT,
17+
TITLE,
18+
BOSS_BAR,
19+
ACTION_BAR
20+
}

0 commit comments

Comments
 (0)