Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [Unreleased]

### Added
- GH-22: Always on healthbars for NAME or SCOREBOARD via special-case `always` duration (@tajobe)

## [0.4.0] - 2025-02-17
### Changed
- MC 1.21, Kotlin 2.1, Gradle 8.12 (@tajobe)
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
71 changes: 50 additions & 21 deletions src/main/kotlin/org/simplemc/simplehealthbars2/DamageListener.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.entity.EntityDeathEvent
import org.bukkit.event.entity.EntitySpawnEvent
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.plugin.Plugin
import org.simplemc.simplehealthbars2.healthbar.Healthbar
import org.simplemc.simplehealthbars2.healthbar.MobHealthbar
import org.simplemc.simplehealthbars2.healthbar.PlayerHealthbar
import java.util.UUID
Expand All @@ -18,13 +22,34 @@ class DamageListener(
private val playerHealthbars: Map<String?, PlayerHealthbar?>,
private val mobHealthbars: Map<String?, MobHealthbar?>,
) : Listener, AutoCloseable {
private data class RemoveHealthbarTask(val taskId: Int, val task: () -> Unit)
private data class RemoveHealthbarTask(val taskId: Int?, val removeAction: () -> Unit)

private val scheduler = Bukkit.getScheduler()
private val removeHealthbarTasks: MutableMap<UUID, RemoveHealthbarTask?> = mutableMapOf()

// <editor-fold desc="Set always on healthbars on spawn/join">
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
fun onEntityDamageEvent(event: EntityDamageByEntityEvent) {
fun onEntitySpawn(event: EntitySpawnEvent) {
val entity = event.entity as? LivingEntity
entity?.healthbar?.let { healthbar ->
if (healthbar.durationTicks == null) {
healthbar(null, entity, 0.0)
}
}
}

@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
fun onPlayerJoin(event: PlayerJoinEvent) {
event.player.healthbar?.let { healthbar ->
if (healthbar.durationTicks == null) {
healthbar(null, event.player, 0.0)
}
}
}
// </editor-fold>

@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
fun onEntityDamageByEntityEvent(event: EntityDamageByEntityEvent) {
val target = event.entity as? LivingEntity ?: return
val source = event.damager as? LivingEntity

Expand All @@ -33,42 +58,46 @@ class DamageListener(
source?.let { healthbar(target, it, 0.0) }
}

@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
fun onPlayerQuit(event: PlayerQuitEvent) = clearEntityHealthbar(event.player)

/**
* Remove the healthbar from dying entities immediately
*/
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
fun onEntityDeathEvent(event: EntityDeathEvent) {
removeHealthbarTasks[event.entity.uniqueId]?.let {
scheduler.cancelTask(it.taskId)
it.task()
}
}
fun onEntityDeathEvent(event: EntityDeathEvent) = clearEntityHealthbar(event.entity)

private fun healthbar(source: LivingEntity?, target: LivingEntity, damage: Double) {
// cancel scheduled healthbar removal and run it now to prepare for new (updated) healthbar
removeHealthbarTasks[target.uniqueId]?.let {
scheduler.cancelTask(it.taskId)
it.task()
clearEntityHealthbar(target)

// update healthbar and schedule its removal task if necessary
val healthbar = target.healthbar
healthbar?.updateHealth(source, target, damage)?.let { removeAction ->
val taskId = healthbar.durationTicks?.let { ticks ->
scheduler.scheduleSyncDelayedTask(plugin, removeAction, ticks)
}
removeHealthbarTasks[target.uniqueId] = RemoveHealthbarTask(taskId, removeAction)
}
}

val healthbar = when (target) {
is Player -> playerHealthbars[target.world.name] ?: playerHealthbars[null]
else -> mobHealthbars[target.world.name] ?: mobHealthbars[null]
private val LivingEntity.healthbar: Healthbar?
get(): Healthbar? = when (this) {
is Player -> playerHealthbars[world.name] ?: playerHealthbars[null]
else -> mobHealthbars[world.name] ?: mobHealthbars[null]
}

// update healthbar and schedule its removal task if available
healthbar?.updateHealth(source, target, damage)?.let {
val taskId = scheduler.scheduleSyncDelayedTask(plugin, it, healthbar.durationTicks)
removeHealthbarTasks[target.uniqueId] = RemoveHealthbarTask(taskId, it)
private fun clearEntityHealthbar(entity: LivingEntity) {
removeHealthbarTasks.remove(entity.uniqueId)?.let {
it.taskId?.let { taskId -> scheduler.cancelTask(taskId) }
it.removeAction()
}
}

/**
* Remember to remove all healthbars on close
*/
override fun close() {
removeHealthbarTasks
.mapNotNull { (_, removeTask) -> removeTask?.task }
.forEach { it() }
removeHealthbarTasks.forEach { (_, removeTask) -> removeTask?.removeAction() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ class SimpleHealthbars2 : JavaPlugin() {
Loaded Healthbar configs:
Player bars:
${barsConfigToString(playerHealthbars)}

Mob bars:
${barsConfigToString(mobHealthbars)}

""".trimIndent()
}

Expand All @@ -87,7 +87,7 @@ class SimpleHealthbars2 : JavaPlugin() {
ScoreboardHealthbar.Config(
useMainScoreboard = config.getBoolean("useMainScoreboard", false),
style = Healthbar.Style.valueOf(checkNotNull(config.getString("style", "ABSOLUTE"))),
duration = Duration.ofSeconds(config.getLong("duration", 5)),
duration = loadBarDuration(config),
),
)
Healthbar.Type.NONE -> null
Expand All @@ -96,12 +96,20 @@ class SimpleHealthbars2 : JavaPlugin() {

private fun loadStringBar(config: ConfigurationSection) = StringHealthbar.Config(
style = Healthbar.Style.valueOf(checkNotNull(config.getString("style", "BAR"))),
duration = Duration.ofSeconds(config.getLong("duration", 5)),
duration = loadBarDuration(config),
length = config.getInt("length", 20),
char = config.getInt("char", 0x25ae).toChar(),
showMobNames = config.getBoolean("showMobNames", true),
)

private fun loadBarDuration(config: ConfigurationSection): Duration? = config.getString("duration").let {
if (it == "always") {
null
} else {
Duration.ofSeconds(it?.toLongOrNull() ?: 5L)
}
}

override fun onDisable() {
listener.close()
Bukkit.getScoreboardManager()?.mainScoreboard?.getObjective(OBJECTIVE_NAME)?.unregister()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ interface Healthbar {

interface Config {
val style: Style
val duration: Duration
val duration: Duration?
}

val config: Config
val durationTicks: Long
get() = config.duration.seconds * 20
val durationTicks: Long?
get() = config.duration?.seconds?.times(20)

/**
* Update target's healthbar with latest health
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ScoreboardHealthbar(override val config: Config) : PlayerHealthbar {
data class Config(
val useMainScoreboard: Boolean = false,
override val style: Healthbar.Style = Healthbar.Style.ABSOLUTE,
override val duration: Duration = Duration.ofSeconds(5),
override val duration: Duration? = Duration.ofSeconds(5),
) : Healthbar.Config

private val objective: Objective
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlin.math.ceil
abstract class StringHealthbar(final override val config: Config) : Healthbar {
data class Config(
override val style: Healthbar.Style = Healthbar.Style.BAR,
override val duration: Duration = Duration.ofSeconds(5),
override val duration: Duration? = Duration.ofSeconds(5),
val length: Int = 20,
val char: Char = 0x25ae.toChar(),
val showMobNames: Boolean = true,
Expand Down