diff --git a/src/commonMain/kotlin/model/Battle.kt b/src/commonMain/kotlin/model/Battle.kt new file mode 100644 index 0000000..2133760 --- /dev/null +++ b/src/commonMain/kotlin/model/Battle.kt @@ -0,0 +1,189 @@ +package model + +import korlibs.datastructure.* +import korlibs.time.* + +class Battle(initializer: LoadAction = LoadAction.ZERO) { + private val _battlers: MutableList = + initializer.battlers.map { Battler(it, initializer.timestamp) }.toMutableList() + private val _pendingEffects: PriorityQueue = + PriorityQueue().apply { addAll(initializer.pendingEffects) } + var currentTime: TimeSpan = initializer.timestamp + private set + + /** Full history entries needed for rewinding. Stored from oldest to newest. */ + private val _history: Deque = Deque() + /** Actions which will be executed at a later timestamp. */ + private val _pendingActions: PriorityQueue = PriorityQueue() + /** Abbreviated history entries used only for saving replay data. Stored from oldest to newest. */ + private val _pastHistory: Queue = Queue() + + val battlers: List = _battlers.map { it.asReadonly } + val pendingEffects: List = _pendingEffects.toList() + val history: List = _history.toList() + val pendingActions: List = _pendingActions.toList() + val pastHistory: List = _pastHistory.toList() + + init { + saveHistoryEntry(initializer) + } + + constructor(actions: List, timestamp: TimeSpan? = null): this( + actions.firstOrNull().run { + when (this) { + is LoadAction -> this + else -> LoadAction.ZERO + } + } + ) { + if (actions.firstOrNull() is LoadAction) { + injectActions(actions.drop(1)) + } else { + injectActions(actions) + } + if (timestamp != null) { + setTimeTo(timestamp) + } + } + + fun injectAction(action: BattleAction) { + when { + action.timestamp > currentTime -> _pendingActions.add(action) + action.timestamp == currentTime -> applyEvent(action) + action.timestamp < currentTime -> { + val present = currentTime + rewindTo(action.timestamp) + applyEvent(action) + advanceTo(present) + } + } + } + + fun injectActions(actions: Iterable) { + actions.forEach { injectAction(it) } + } + + /** Abbreviates [_history] up to and including the given [timestamp], moving it to [_pastHistory]. */ + fun abbreviateHistoryUpTo(timestamp: TimeSpan) { + while (_history.isNotEmpty() && _history.first.timestamp <= timestamp) { + val previous = _history.removeFirst() + if (previous is ActionHistoryEntry) { + _pastHistory.enqueue(previous.action) + } + } + } + + fun setTimeTo(timestamp: TimeSpan) { + if (timestamp == currentTime) { + // then we're done already! + } else if (timestamp > currentTime){ + advanceTo(timestamp) + } else { + rewindTo(timestamp) + } + } + + private fun rewindTo(timestamp: TimeSpan) { + if (_history.isEmpty()) { + throw IllegalStateException( + "tried to rewind to ${timestamp.inWholeMilliseconds}ms, but have no full history at all") + } + + if (_history.first.timestamp > timestamp) { + throw IllegalStateException( + "tried to rewind to ${timestamp.inWholeMilliseconds}ms, " + + "but only have full history back to ${_history.first.timestamp.inWholeMilliseconds}") + } + + while (_history.isNotEmpty() && _history.last.timestamp > timestamp) { + val previous = _history.removeLast() + if (previous is ActionHistoryEntry) { + _pendingActions.add(previous.action) + } + } + + applyHistoryEntry(_history.last) + if (timestamp > currentTime) { + advanceTo(timestamp) + } + } + + private fun advanceTo(timestamp: TimeSpan) { + while (nextEventAt < timestamp) { + val nextEvent = if (nextActionAt < nextEffectAt) { + _pendingActions.removeHead() + } else { + _pendingEffects.removeHead() + } + applyEvent(nextEvent) + } + advanceTimeTo(timestamp) + } + + val nextActionAt: TimeSpan get() = nextAction?.timestamp ?: TimeSpan.INFINITE + val nextEffectAt: TimeSpan get() = nextEffect?.timestamp ?: TimeSpan.INFINITE + val nextEventAt: TimeSpan get() = nextEvent?.timestamp ?: TimeSpan.INFINITE + + val nextAction: BattleAction? get() = when { + _pendingActions.isNotEmpty() -> _pendingActions.head + else -> null + } + + val nextEffect: BattleEffect? get() = when { + _pendingEffects.isNotEmpty() -> _pendingEffects.head + else -> null + } + + val nextEvent: TimedEvent? get() { + val action = nextAction + val effect = nextEffect + return when { + action == null || (effect != null && effect < action) -> effect + else -> action + } + } + + private fun advanceTimeTo(timestamp: TimeSpan) { + if (currentTime == timestamp) { + return + } + currentTime = timestamp + _battlers.forEach { it.advanceTo(currentTime) } + } + + private fun applyEvent(event: TimedEvent) { + advanceTimeTo(event.timestamp) + with(event) { execute() } + saveHistoryEntry(event) + } + + private fun saveHistoryEntry(entry: TimedEvent) { + val archivedBattlers = _battlers.map { it.asSaved } + val archivedEffects = _pendingEffects.toList() + _history.add(when (entry) { + is LoadAction -> ActionHistoryEntry( + action = entry, + battlers = entry.battlers, + pendingEffects = entry.pendingEffects, + ) + is BattleAction -> ActionHistoryEntry( + action = entry, + battlers = archivedBattlers, + pendingEffects = archivedEffects, + ) + is BattleEffect -> EffectHistoryEntry( + effect = entry, + battlers = archivedBattlers, + pendingEffects = archivedEffects, + ) + }) + } + + private fun applyHistoryEntry(entry: HistoryEntry) { + currentTime = entry.timestamp + _pendingEffects.clear() + _pendingEffects.addAll(entry.pendingEffects) + _battlers.clear() + _battlers.addAll(entry.battlers.map { Battler(it, entry.timestamp) }) + } +} diff --git a/src/commonMain/kotlin/model/Battler.kt b/src/commonMain/kotlin/model/Battler.kt index df15e27..d0abc4e 100644 --- a/src/commonMain/kotlin/model/Battler.kt +++ b/src/commonMain/kotlin/model/Battler.kt @@ -2,31 +2,116 @@ package model import korlibs.math.* import korlibs.time.* +import kotlin.jvm.* import kotlin.math.* -data class Battler( - var name: String, - var composure: Double, - var health: Double, - var maxHealth: Double, - var energy: Double, - var stamina: Double, - var maxStamina: Double, - - var shaken: Boolean = false, - var shakenTimes: Int = 0, - var composurePausedUntil: TimeSpan = TimeSpan.ZERO, - var energyPausedUntil: TimeSpan = TimeSpan.ZERO, - var currentUpdate: TimeSpan = TimeSpan.ZERO, +data class Battler ( + val id: Id, + + private var _name: String, + private var _composure: Double, + private var _health: Double, + private var _maxHealth: Double, + private var _energy: Double, + private var _stamina: Double, + private var _maxStamina: Double, + + private var _shaken: Boolean = false, + private var _shakenTimes: Int = 0, + private var _composurePausedUntil: TimeSpan = TimeSpan.ZERO, + private var _energyPausedUntil: TimeSpan = TimeSpan.ZERO, + private var _currentUpdate: TimeSpan = TimeSpan.ZERO, + private var lastUpdate: TimeSpan = TimeSpan.ZERO, - private var lastShaken: Boolean = shaken, - private var lastComposure: Double = composure, - private var lastEnergy: Double = energy, - private var lastStamina: Double = stamina, + private var lastShaken: Boolean = _shaken, + private var lastComposure: Double = _composure, + private var lastEnergy: Double = _energy, + private var lastStamina: Double = _stamina, ) { - constructor(name: String, health: Double, stamina: Double) : - this(name = name, composure = health, health = health, maxHealth = health, - energy = stamina, stamina = stamina, maxStamina = stamina) + @JvmInline + value class Id(val v: Int) + + class Editable (private val battler: Battler){ + val id: Id get() = battler.id + var name: String get() = battler.name + set(v) { battler._name = v } + var composure: Double get() = battler.composure + set(v) { battler._composure = v } + var health: Double get() = battler.health + set(v) { battler._health = v } + var maxHealth: Double get() = battler.maxHealth + set(v) { battler._maxHealth = v } + var energy: Double get() = battler.energy + set(v) { battler._energy = v } + var stamina: Double get() = battler.stamina + set(v) { battler._stamina = v } + var maxStamina: Double get() = battler.maxStamina + set(v) { battler._maxStamina = v } + var shaken: Boolean get() = battler.shaken + set(v) { battler._shaken = v } + var shakenTimes: Int get() = battler.shakenTimes + set(v) { battler._shakenTimes = v } + var composurePausedUntil: TimeSpan get() = battler.composurePausedUntil + set(v) { battler._composurePausedUntil = v } + var energyPausedUntil: TimeSpan get() = battler.energyPausedUntil + set(v) { battler._energyPausedUntil = v } + val currentUpdate: TimeSpan get() = battler.currentUpdate + + fun becomeShaken() { + composure = health.coerceAtMost(0.0) + if (shaken) { + return + } + composurePausedUntil = (currentUpdate + battler.composureRecoveryDelay).coerceAtLeast(composurePausedUntil) + shaken = true + shakenTimes += 1 + } + + fun inflictComposureDamage(damage: Double) { + if (shaken) { + return + } + composurePausedUntil = + (currentUpdate + battler.composureChargeDelay).coerceAtLeast(composurePausedUntil) + composure -= damage + if (composure <= 0) { + becomeShaken() + } + } + + fun inflictHealthDamage(damage: Double) { + composurePausedUntil = (currentUpdate + battler.composureRecoveryDelay).coerceAtLeast(composurePausedUntil) + health -= damage + composure -= damage + if (composure <= 0) { + becomeShaken() + } + } + + fun spendEnergy(cost: Double) { + energyPausedUntil = (currentUpdate + battler.energyChargeDelay).coerceAtLeast(energyPausedUntil) + energy = (energy - cost).coerceAtLeast(0.0) + } + + fun spendStamina(cost: Double) { + energyPausedUntil = (currentUpdate + battler.energyChargeDelay).coerceAtLeast(energyPausedUntil) + stamina = (stamina - cost).coerceAtLeast(0.0) + energy = (energy - cost).coerceAtLeast(0.0) + } + } + + val name: String get() = _name + val composure: Double get() = _composure + val health: Double get() = _health + val maxHealth: Double get() = _maxHealth + val energy: Double get() = _energy + val stamina: Double get() = _stamina + val maxStamina: Double get() = _maxStamina + val shaken: Boolean get() = _shaken + val shakenTimes: Int get() = _shakenTimes + val composurePausedUntil: TimeSpan get() = _composurePausedUntil + val energyPausedUntil: TimeSpan get() = _energyPausedUntil + val currentUpdate: TimeSpan get() = _currentUpdate private val composureChargeTime: TimeSpan get() = TimeSpan(45_000.0 * 1.2.pow(shakenTimes - 1)) private val composureChargeDelay: TimeSpan get() = TimeSpan(2_000.0 * 1.2.pow(shakenTimes - 1)) @@ -53,31 +138,31 @@ data class Battler( val halfHealth = health/2.0 val dtRecover = composureRecoveryTime * (halfHealth - lastComposure.coerceAtLeast(0.0)) / halfHealth if (dtComposure > dtRecover) { - composure = halfHealth - shaken = false + _composure = halfHealth + _shaken = false dtComposure -= dtRecover } else { - composure = ( + _composure = ( lastComposure.coerceAtLeast(0.0) + dtComposure.milliseconds * halfHealth / composureRecoveryTime.milliseconds) - shaken = lastShaken + _shaken = lastShaken dtComposure = TimeSpan.ZERO } } else { - composure = lastComposure - shaken = lastShaken + _composure = lastComposure + _shaken = lastShaken } if (dtComposure.isPositive()) { if (health > 0) { val lastChargeTime = TimeSpan( sqrt(4 * composureChargeCoefficient * composure) / (2 * composureChargeCoefficient) ) - composure = if (dtComposure + lastChargeTime >= composureChargeTime) { + _composure = if (dtComposure + lastChargeTime >= composureChargeTime) { health } else { (composureChargeCoefficient * (lastChargeTime + dtComposure).milliseconds.squared()) } } else { - composure = health + _composure = health } } } @@ -97,27 +182,27 @@ data class Battler( if (lastStamina > 0) { val dtToFull = energyChargeTime * (lastStamina - lastEnergy) / lastStamina if (dtEnergy >= dtToFull) { - energy = lastStamina + _energy = lastStamina dtEnergy -= dtToFull } else { - energy = lastEnergy + (lastStamina * dtEnergy.milliseconds) / energyChargeTime.milliseconds + _energy = lastEnergy + (lastStamina * dtEnergy.milliseconds) / energyChargeTime.milliseconds dtEnergy = TimeSpan.ZERO } } else { - energy = lastStamina + _energy = lastStamina } } if (dtEnergy.isPositive() && lastStamina < maxStamina) { - stamina = min( + _stamina = min( maxStamina, lastStamina + (maxStamina * dtEnergy.milliseconds) / staminaChargeTime.milliseconds) - energy = stamina + _energy = stamina } } } fun advanceTo(t: TimeSpan) { - currentUpdate = t + _currentUpdate = t if (composure < health) { advanceComposureTo(t) } @@ -126,57 +211,114 @@ data class Battler( } } - fun becomeShaken() { - composure = health.coerceAtMost(0.0) - if (shaken) { - return - } - composurePausedUntil = (currentUpdate + composureRecoveryDelay).coerceAtLeast(composurePausedUntil) - shaken = true - shakenTimes += 1 - } + fun updateAt(t: TimeSpan, block: Editable.() -> Unit) { + advanceTo(t) - fun inflictComposureDamage(damage: Double) { - if (shaken) { - return - } - composurePausedUntil = (currentUpdate + composureChargeDelay).coerceAtLeast(composurePausedUntil) - composure -= damage - if (composure <= 0) { - becomeShaken() - } - } + block(Editable(this)) - fun inflictHealthDamage(damage: Double) { - composurePausedUntil = (currentUpdate + composureRecoveryDelay).coerceAtLeast(composurePausedUntil) - health -= damage - composure -= damage - if (composure <= 0) { - becomeShaken() - } + lastUpdate = currentUpdate + lastShaken = shaken + lastComposure = composure + lastEnergy = energy + lastStamina = stamina } - fun spendEnergy(cost: Double) { - energyPausedUntil = (currentUpdate + energyChargeDelay).coerceAtLeast(energyPausedUntil) - energy = (energy - cost).coerceAtLeast(0.0) - } + val asSaved: Saved get() = Saved( + id = id, + + name = name, + composure = composure, + health = health, + maxHealth = maxHealth, + energy = energy, + stamina = stamina, + maxStamina = maxStamina, + + shaken = shaken, + shakenTimes = shakenTimes, + composurePausedUntil = composurePausedUntil, + energyPausedUntil = energyPausedUntil + ) - fun spendStamina(cost: Double) { - energyPausedUntil = (currentUpdate + energyChargeDelay).coerceAtLeast(energyPausedUntil) - stamina = (stamina - cost).coerceAtLeast(0.0) - energy = (energy - cost).coerceAtLeast(0.0) + fun revertTo(previous: Saved, at: TimeSpan) { + _name = previous.name + _composure = previous.composure + _health = previous.health + _maxHealth = previous.maxHealth + _energy = previous.energy + _stamina = previous.stamina + _maxStamina = previous.maxStamina + + _shaken = previous.shaken + _shakenTimes = previous.shakenTimes + _composurePausedUntil = previous.composurePausedUntil + _energyPausedUntil = previous.energyPausedUntil + + _currentUpdate = at + lastUpdate = at + lastShaken = previous.shaken + lastComposure = previous.composure + lastEnergy = previous.energy + lastStamina = previous.stamina } - fun updateAt(t: TimeSpan, block: Battler.() -> Unit) { - advanceTo(t) + constructor(saved: Saved, at: TimeSpan) : this( + id = saved.id, - block(this) + _name = saved.name, + _composure = saved.composure, + _health = saved.health, + _maxHealth = saved.maxHealth, + _energy = saved.energy, + _stamina = saved.stamina, + _maxStamina = saved.maxStamina, - lastUpdate = currentUpdate - lastShaken = shaken - lastComposure = composure - lastEnergy = energy - lastStamina = stamina + _shaken = saved.shaken, + _shakenTimes = saved.shakenTimes, + _composurePausedUntil = saved.composurePausedUntil, + _energyPausedUntil = saved.energyPausedUntil, + + _currentUpdate = at, + lastUpdate = at, + lastShaken = saved.shaken, + lastComposure = saved.composure, + lastEnergy = saved.energy, + lastStamina = saved.stamina, + ) + + data class Saved( + val id: Id, + + val name: String, + val composure: Double, + val health: Double = composure, + val maxHealth: Double = health, + val energy: Double, + val stamina: Double = energy, + val maxStamina: Double = stamina, + + val shaken: Boolean = false, + val shakenTimes: Int = 0, + val composurePausedUntil: TimeSpan = TimeSpan.ZERO, + val energyPausedUntil: TimeSpan = TimeSpan.ZERO, + ) + + val asReadonly get() = Readonly(this) + + class Readonly (private val battler: Battler) { + val id: Id get() = battler.id + val name: String get() = battler.name + val composure: Double get() = battler.composure + val health: Double get() = battler.health + val maxHealth: Double get() = battler.maxHealth + val energy: Double get() = battler.energy + val stamina: Double get() = battler.stamina + val maxStamina: Double get() = battler.maxStamina + val shaken: Boolean get() = battler.shaken + val shakenTimes: Int get() = battler.shakenTimes + val composurePausedUntil: TimeSpan get() = battler.composurePausedUntil + val energyPausedUntil: TimeSpan get() = battler.energyPausedUntil + val currentUpdate: TimeSpan get() = battler.currentUpdate + val asSaved: Saved get() = battler.asSaved } } - diff --git a/src/commonMain/kotlin/model/HistoryEntry.kt b/src/commonMain/kotlin/model/HistoryEntry.kt new file mode 100644 index 0000000..9b5c2dd --- /dev/null +++ b/src/commonMain/kotlin/model/HistoryEntry.kt @@ -0,0 +1,29 @@ +package model + +import korlibs.time.* + +sealed interface HistoryEntry: Comparable { + val timestamp: TimeSpan + val pendingEffects: List + val battlers: List + + override fun compareTo(other: HistoryEntry): Int { + return timestamp.compareTo(other.timestamp) + } +} + +data class ActionHistoryEntry( + val action: BattleAction, + override val pendingEffects: List, + override val battlers: List, +): HistoryEntry { + override val timestamp: TimeSpan get() = action.timestamp +} + +data class EffectHistoryEntry( + val effect: BattleEffect, + override val pendingEffects: List, + override val battlers: List, +): HistoryEntry { + override val timestamp: TimeSpan get() = effect.timestamp +} diff --git a/src/commonMain/kotlin/model/TimedEvent.kt b/src/commonMain/kotlin/model/TimedEvent.kt new file mode 100644 index 0000000..ab80a8e --- /dev/null +++ b/src/commonMain/kotlin/model/TimedEvent.kt @@ -0,0 +1,31 @@ +package model + +import korlibs.time.* + +sealed interface TimedEvent: Comparable { + val timestamp: TimeSpan + fun Battle.execute() + + override fun compareTo(other: TimedEvent): Int { + return timestamp.compareTo(other.timestamp) + } +} + +/** A [BattleEffect] represents some kind of timed impact on the game as a result of a [BattleAction]. */ +sealed interface BattleEffect: TimedEvent + +/** A [BattleAction] represents some kind of input from a player. */ +sealed interface BattleAction: TimedEvent + +data class LoadAction( + override val timestamp: TimeSpan, + val battlers: List, + val pendingEffects: List) : BattleAction { + override fun Battle.execute() { + throw AssertionError("LoadActions should never be executed normally.") + } + + companion object{ + val ZERO = LoadAction(timestamp = TimeSpan.ZERO, battlers = listOf(), pendingEffects = listOf()) + } +} diff --git a/src/commonMain/kotlin/scenes/BattleScene.kt b/src/commonMain/kotlin/scenes/BattleScene.kt index 1bc321b..c5478b3 100644 --- a/src/commonMain/kotlin/scenes/BattleScene.kt +++ b/src/commonMain/kotlin/scenes/BattleScene.kt @@ -1,70 +1,68 @@ package scenes -import korlibs.event.* -import korlibs.korge.input.* import korlibs.korge.scene.* import korlibs.korge.view.* import korlibs.time.* import model.* import views.* +import kotlin.math.* class BattleScene : Scene() { - private val us = BattlerView(Battler("Us", 200.0, 300.0)) - private val them = BattlerView(Battler("Them", 200.0, 300.0)) + private val battle = Battle(LoadAction( + timestamp = TimeSpan.ZERO, + battlers = listOf( + Battler.Saved( + id = Battler.Id(0), + name = "Us", + composure = 0.0, + health = 150.0, + maxHealth = 200.0, + energy = 0.0, + stamina = 150.0, + maxStamina = 300.0, + + shaken = false, + shakenTimes = 0, + composurePausedUntil = TimeSpan.ZERO, + energyPausedUntil = TimeSpan.ZERO, + ), + Battler.Saved( + id = Battler.Id(1), + name = "Them", + composure = 0.0, + health = 150.0, + maxHealth = 200.0, + energy = 0.0, + stamina = 150.0, + maxStamina = 300.0, + + shaken = false, + shakenTimes = 0, + composurePausedUntil = TimeSpan.ZERO, + energyPausedUntil = TimeSpan.ZERO, + ) + ), + pendingEffects = listOf() + )) override suspend fun SContainer.sceneInit() { - us.xy(sceneWidth - (us.width + 5), sceneHeight - (us.height + 5)) - addChild(us) - them.xy(5, 5) - addChild(them) + val usView = BattlerView(battle.battlers[0]) + usView.xy(sceneWidth - (usView.width + 5), sceneHeight - (usView.height + 5)) + addChild(usView) + + val themView = BattlerView(battle.battlers[1]) + themView.xy(5, 5) + addChild(themView) var time = TimeSpan.ZERO + var fractional = 0.0 addUpdater { - time += it - } - - keys { - downRepeating(Key.Q) { - them.battler.updateAt(time) { - them.battler.composure = (them.battler.composure + 5.0).coerceAtMost(them.battler.health) - } - } - downRepeating(Key.A) { - them.battler.updateAt(time) { - inflictComposureDamage(5.0) - } - } - downRepeating(Key.W) { - them.battler.updateAt(time) { - them.battler.health = (them.battler.health + 5.0).coerceAtMost(them.battler.maxHealth) - } - } - downRepeating(Key.S) { - them.battler.updateAt(time) { - inflictHealthDamage(5.0) - } - } - downRepeating(Key.E) { - them.battler.updateAt(time) { - them.battler.energy = (them.battler.energy + 5.0).coerceAtMost(them.battler.stamina) - } - } - downRepeating(Key.D) { - them.battler.updateAt(time) { - spendEnergy(5.0) - } - } - downRepeating(Key.R) { - them.battler.updateAt(time) { - them.battler.stamina = (them.battler.stamina + 5.0).coerceAtMost(them.battler.maxStamina) - } - } - downRepeating(Key.F) { - them.battler.updateAt(time) { - spendStamina(5.0) - } - } + val step = it.milliseconds + fractional + val whole = floor(step) + fractional = step - whole + time += TimeSpan(whole) + battle.setTimeTo(time) } } diff --git a/src/commonMain/kotlin/views/BattlerView.kt b/src/commonMain/kotlin/views/BattlerView.kt index 74e49dc..2f5d2a5 100644 --- a/src/commonMain/kotlin/views/BattlerView.kt +++ b/src/commonMain/kotlin/views/BattlerView.kt @@ -1,6 +1,6 @@ package views -import korlibs.image.paint.* +import korlibs.image.color.* import korlibs.image.vector.* import korlibs.korge.ui.* import korlibs.korge.view.* @@ -8,7 +8,7 @@ import korlibs.math.geom.* import model.* import kotlin.math.* -class BattlerView (val battler: Battler) : UIContainer(Size(200.0, 40.0)) { +class BattlerView (val battler: Battler.Readonly) : UIContainer(Size(200.0, 40.0)) { private val staminaBar = graphics { drawStaminaBar() @@ -26,7 +26,6 @@ class BattlerView (val battler: Battler) : UIContainer(Size(200.0, 40.0)) { init { addUpdater { - battler.advanceTo(battler.currentUpdate + it) updateBattler() } } @@ -34,28 +33,28 @@ class BattlerView (val battler: Battler) : UIContainer(Size(200.0, 40.0)) { private fun ShapeBuilder.drawHealthBar() { if (battler.health > -battler.maxHealth) { fillRect(0, 10, 200 * (1 + battler.health.coerceAtMost(0.0) / battler.maxHealth), 20) - fill(ColorPaint(0x5f, 0x08, 0x08, 0xff)) + fill(RGBA(0x5f, 0x08, 0x08, 0xff)) } if (battler.health > 0) { fillRect(0.0, 10.0, battler.health * 200.0 / battler.maxHealth, 20.0) - fill(ColorPaint(0x8f, 0x10, 0x10, 0xff)) + fill(RGBA(0x8f, 0x10, 0x10, 0xff)) } if (battler.composure > 0) { fillRect(0.0, 10.0, battler.composure * 200.0 / battler.maxHealth, 20.0) - fill(ColorPaint(0x10, 0xcf, 0x5f, 0xff)) + fill(RGBA(0x10, 0xcf, 0x5f, 0xff)) } } private fun ShapeBuilder.drawStaminaBar() { fillRect(50, 30, 150, 10) - fill(ColorPaint(0x10, 0x10, 0x10, 0xff)) + fill(RGBA(0x10, 0x10, 0x10, 0xff)) if (battler.stamina > 0) { fillRect(50.0, 30.0, battler.stamina * 150.0 / battler.maxStamina, 10.0) - fill(ColorPaint(0x10, 0x5f, 0x7f, 0xff)) + fill(RGBA(0x10, 0x5f, 0x7f, 0xff)) } if (battler.energy > 0) { fillRect(50.0, 30.0, battler.energy * 150.0 / battler.maxStamina, 10.0) - fill(ColorPaint(0x10, 0x9f, 0xaf, 0xff)) + fill(RGBA(0x10, 0x9f, 0xaf, 0xff)) } }