battle/battler event API + history buttons in test scene

main
Mari 5 months ago
parent 30a5b452d4
commit 0c2b8446d4
  1. 131
      src/commonMain/kotlin/model/Battle.kt
  2. 25
      src/commonMain/kotlin/model/BattleAction.kt
  3. 17
      src/commonMain/kotlin/model/BattleEffect.kt
  4. 18
      src/commonMain/kotlin/model/Battler.kt
  5. 45
      src/commonMain/kotlin/model/HistoryEntry.kt
  6. 27
      src/commonMain/kotlin/model/TimedEvent.kt
  7. 32
      src/commonMain/kotlin/scenes/BattleScene.kt

@ -1,9 +1,12 @@
package model
import korlibs.datastructure.*
import korlibs.datastructure.iterators.*
import korlibs.io.lang.*
import korlibs.time.*
import kotlinx.serialization.Serializable
class Battle(initializer: LoadAction = LoadAction.ZERO) {
class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
private val _battlers: MutableList<Battler> =
initializer.battlers.map { Battler(it, initializer.timestamp) }.toMutableList()
private val _pendingEffects: PriorityQueue<BattleEffect> =
@ -12,37 +15,70 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
private set
/** Full history entries needed for rewinding. Stored from oldest to newest. */
private val _history: Deque<HistoryEntry> = Deque()
private val _history: Deque<HistoryEntry> = Deque<HistoryEntry>().also { it.add(initializer) }
/** Actions which will be executed at a later timestamp. */
private val _pendingActions: PriorityQueue<BattleAction> = PriorityQueue()
/** Abbreviated history entries used only for saving replay data. Stored from oldest to newest. */
private val _pastHistory: Queue<BattleAction> = Queue()
val battlers: List<Battler.Readonly> = _battlers.map { it.asReadonly }
val pendingEffects: List<BattleEffect> = _pendingEffects.toList()
val history: List<HistoryEntry> = _history.toList()
val pendingActions: List<BattleAction> = _pendingActions.toList()
val pastHistory: List<BattleAction> = _pastHistory.toList()
val battlers: List<Battler.Readonly> get() = _battlers.map { it.asReadonly }
val pendingEffects: List<BattleEffect> get() = _pendingEffects.toList()
val history: List<HistoryEntry> get() = _history.toList()
val pendingActions: List<BattleAction> get() = _pendingActions.toList()
val pastHistory: List<BattleAction> get() = _pastHistory.toList()
init {
saveHistoryEntry(initializer)
private val eventAPI: EventAPI by lazy { EventAPI(this) }
constructor(initializer: HistoryEntry.Load, actions: List<BattleAction>, timestamp: TimeSpan? = null):
this(initializer) {
injectActions(actions)
if (timestamp != null) {
setTimeTo(timestamp)
}
}
constructor(actions: List<BattleAction>, timestamp: TimeSpan? = null): this(
actions.firstOrNull().run {
when (this) {
is LoadAction -> this
else -> LoadAction.ZERO
class EventAPI(private val battle: Battle) {
val battlers: List<Battler.Readonly> get() = battle.battlers
fun addBattler(battler: Battler.Saved) {
if (battle._battlers.any { it.id == battler.id }) {
throw IllegalArgumentException("Can't add a battler who's already in the fight!")
}
battle._battlers.add(Battler(saved = battler, at = battle.currentTime))
}
) {
if (actions.firstOrNull() is LoadAction) {
injectActions(actions.drop(1))
} else {
injectActions(actions)
fun removeBattler(id: Battler.Id) {
battle._battlers.fastIterateRemove { it.id == id }
}
if (timestamp != null) {
setTimeTo(timestamp)
fun battlers(block: Battler.EventAPI.() -> Unit) {
battle._battlers.forEach { it.updateAt(battle.currentTime, block) }
}
fun battler(id: Battler.Id): Battler.Readonly? = battle._battlers.find { it.id == id }?.asReadonly
fun battler(id: Battler.Id, block: Battler.EventAPI.() -> Unit) {
battle._battlers.find { it.id == id }?.updateAt(battle.currentTime, block)
}
fun Battler.Readonly.update(block: Battler.EventAPI.() -> Unit) {
battler(id, block)
}
fun Battler.Readonly.remove() {
removeBattler(id)
}
fun injectEffect(effect: BattleEffect) {
when {
effect.timestamp > battle.currentTime -> battle._pendingEffects.add(effect)
else -> throw InvalidArgumentException(
"can't inject an effect that would take place now or in the past: $effect")
}
}
fun injectEffects(effects: Iterable<BattleEffect>) {
effects.forEach { injectEffect(it) }
}
}
@ -67,7 +103,7 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
fun abbreviateHistoryUpTo(timestamp: TimeSpan) {
while (_history.isNotEmpty() && _history.first.timestamp <= timestamp) {
val previous = _history.removeFirst()
if (previous is ActionHistoryEntry) {
if (previous is HistoryEntry.Action) {
_pastHistory.enqueue(previous.action)
}
}
@ -97,7 +133,7 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
while (_history.isNotEmpty() && _history.last.timestamp > timestamp) {
val previous = _history.removeLast()
if (previous is ActionHistoryEntry) {
if (previous is HistoryEntry.Action) {
_pendingActions.add(previous.action)
}
}
@ -153,7 +189,9 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
private fun applyEvent(event: TimedEvent) {
advanceTimeTo(event.timestamp)
with(event) { execute() }
with(event) { with(eventAPI) {
execute()
} }
saveHistoryEntry(event)
}
@ -161,17 +199,12 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
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(
is BattleAction -> HistoryEntry.Action(
action = entry,
battlers = archivedBattlers,
pendingEffects = archivedEffects,
)
is BattleEffect -> EffectHistoryEntry(
is BattleEffect -> HistoryEntry.Effect(
effect = entry,
battlers = archivedBattlers,
pendingEffects = archivedEffects,
@ -183,7 +216,39 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
currentTime = entry.timestamp
_pendingEffects.clear()
_pendingEffects.addAll(entry.pendingEffects)
_battlers.clear()
_battlers.addAll(entry.battlers.map { Battler(it, entry.timestamp) })
_battlers.retainAll { live -> entry.battlers.any { saved -> saved.id == live.id } }
_battlers.fastForEach {
live -> live.revertTo(entry.battlers.find { saved -> saved.id == live.id }!!, entry.timestamp) }
if (_battlers.size < entry.battlers.size) {
entry.battlers.forEach { saved ->
if (!_battlers.any { live -> live.id == saved.id }) {
_battlers.add(Battler(saved, entry.timestamp))
}
}
}
}
val asSaved: Saved get() = Saved(
currentTime = currentTime,
initializer = initializer,
history = buildList {
addAll(pastHistory)
_history.forEach {
if (it is HistoryEntry.Action) {
add(it.action)
}
}
addAll(pendingActions)
}
)
constructor(saved: Saved):
this(initializer = saved.initializer, actions = saved.history, timestamp = saved.currentTime)
@Serializable
data class Saved(
val currentTime: TimeSpan,
val initializer: HistoryEntry.Load,
val history: List<BattleAction>,
)
}

@ -0,0 +1,25 @@
package model
import korlibs.time.*
import kotlinx.serialization.Serializable
@Serializable
/** A [BattleAction] represents some kind of input from a player. */
sealed class BattleAction: TimedEvent()
@Serializable
data class MultiHitAction(
override val timestamp: TimeSpan,
val target: Battler.Id,
val hits: Int,
val firstHitDelay: TimeSpan,
val subsequentHitDelay: TimeSpan,
val damage: Double,
): BattleAction() {
override fun Battle.EventAPI.execute() {
injectEffect(DamageEffect(timestamp + firstHitDelay, target, damage))
for (hit in 2..hits) {
injectEffect(DamageEffect(timestamp + firstHitDelay + subsequentHitDelay * (hit - 1), target, damage))
}
}
}

@ -0,0 +1,17 @@
package model
import korlibs.time.*
import kotlinx.serialization.Serializable
@Serializable
/** A [BattleEffect] represents some kind of timed impact on the game as a result of a [BattleAction]. */
sealed class BattleEffect: TimedEvent()
@Serializable
data class DamageEffect(override val timestamp: TimeSpan, val id: Battler.Id, val damage: Double): BattleEffect() {
override fun Battle.EventAPI.execute() {
battler(id) {
inflictComposureDamage(damage)
}
}
}

@ -2,6 +2,7 @@ package model
import korlibs.math.*
import korlibs.time.*
import kotlinx.serialization.Serializable
import kotlin.jvm.*
import kotlin.math.*
@ -28,10 +29,12 @@ data class Battler (
private var lastEnergy: Double = _energy,
private var lastStamina: Double = _stamina,
) {
@JvmInline
@JvmInline @Serializable
value class Id(val v: Int)
class Editable (private val battler: Battler){
private val eventAPI: EventAPI by lazy { EventAPI(this) }
class EventAPI (private val battler: Battler){
val id: Id get() = battler.id
var name: String get() = battler.name
set(v) { battler._name = v }
@ -57,6 +60,8 @@ data class Battler (
set(v) { battler._energyPausedUntil = v }
val currentUpdate: TimeSpan get() = battler.currentUpdate
val asSaved: Saved get() = battler.asSaved
fun becomeShaken() {
composure = health.coerceAtMost(0.0)
if (shaken) {
@ -98,6 +103,10 @@ data class Battler (
stamina = (stamina - cost).coerceAtLeast(0.0)
energy = (energy - cost).coerceAtLeast(0.0)
}
fun revertTo(saved: Saved) {
battler.revertTo(previous = saved, at = currentUpdate)
}
}
val name: String get() = _name
@ -211,10 +220,10 @@ data class Battler (
}
}
fun updateAt(t: TimeSpan, block: Editable.() -> Unit) {
fun updateAt(t: TimeSpan, block: EventAPI.() -> Unit) {
advanceTo(t)
block(Editable(this))
block(eventAPI)
lastUpdate = currentUpdate
lastShaken = shaken
@ -286,6 +295,7 @@ data class Battler (
lastStamina = saved.stamina,
)
@Serializable
data class Saved(
val id: Id,

@ -1,6 +1,7 @@
package model
import korlibs.time.*
import kotlinx.serialization.*
sealed interface HistoryEntry: Comparable<HistoryEntry> {
val timestamp: TimeSpan
@ -10,20 +11,36 @@ sealed interface HistoryEntry: Comparable<HistoryEntry> {
override fun compareTo(other: HistoryEntry): Int {
return timestamp.compareTo(other.timestamp)
}
}
data class ActionHistoryEntry(
val action: BattleAction,
override val pendingEffects: List<BattleEffect>,
override val battlers: List<Battler.Saved>,
): HistoryEntry {
override val timestamp: TimeSpan get() = action.timestamp
}
@Serializable
@SerialName("load")
data class Load(
override val timestamp: TimeSpan,
override val pendingEffects: List<BattleEffect>,
override val battlers: List<Battler.Saved>,
): HistoryEntry {
companion object{
val ZERO = Load(timestamp = TimeSpan.ZERO, battlers = listOf(), pendingEffects = listOf())
}
}
data class EffectHistoryEntry(
val effect: BattleEffect,
override val pendingEffects: List<BattleEffect>,
override val battlers: List<Battler.Saved>,
): HistoryEntry {
override val timestamp: TimeSpan get() = effect.timestamp
@Serializable
@SerialName("histAction")
data class Action(
val action: BattleAction,
override val pendingEffects: List<BattleEffect>,
override val battlers: List<Battler.Saved>,
): HistoryEntry {
override val timestamp: TimeSpan get() = action.timestamp
}
@Serializable
@SerialName("histFX")
data class Effect(
val effect: BattleEffect,
override val pendingEffects: List<BattleEffect>,
override val battlers: List<Battler.Saved>,
): HistoryEntry {
override val timestamp: TimeSpan get() = effect.timestamp
}
}

@ -1,31 +1,14 @@
package model
import korlibs.time.*
import kotlinx.serialization.*
sealed interface TimedEvent: Comparable<TimedEvent> {
val timestamp: TimeSpan
fun Battle.execute()
@Serializable
sealed class TimedEvent: Comparable<TimedEvent> {
abstract val timestamp: TimeSpan
abstract fun Battle.EventAPI.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<Battler.Saved>,
val pendingEffects: List<BattleEffect>) : 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())
}
}

@ -1,5 +1,7 @@
package scenes
import korlibs.event.*
import korlibs.korge.input.*
import korlibs.korge.scene.*
import korlibs.korge.view.*
import korlibs.time.*
@ -8,13 +10,13 @@ import views.*
import kotlin.math.*
class BattleScene : Scene() {
private val battle = Battle(LoadAction(
private val battle = Battle(HistoryEntry.Load(
timestamp = TimeSpan.ZERO,
battlers = listOf(
Battler.Saved(
id = Battler.Id(0),
name = "Us",
composure = 0.0,
composure = 150.0,
health = 150.0,
maxHealth = 200.0,
energy = 0.0,
@ -29,7 +31,7 @@ class BattleScene : Scene() {
Battler.Saved(
id = Battler.Id(1),
name = "Them",
composure = 0.0,
composure = 150.0,
health = 150.0,
maxHealth = 200.0,
energy = 0.0,
@ -64,6 +66,30 @@ class BattleScene : Scene() {
time += TimeSpan(whole)
battle.setTimeTo(time)
}
keys {
justDown(Key.R) {
time = TimeSpan.ZERO
fractional = 0.0
battle.setTimeTo(time)
}
justDown(Key.F) {
time += TimeSpan(10_000.0)
battle.setTimeTo(time)
}
justDown(Key.V) {
battle.injectAction(MultiHitAction(
timestamp = time,
target = Battler.Id(1),
hits = 5,
firstHitDelay = TimeSpan(500.0),
subsequentHitDelay = TimeSpan(100.0),
damage = 5.0,
))
}
}
}
override suspend fun SContainer.sceneMain() {

Loading…
Cancel
Save