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 package model
import korlibs.datastructure.* import korlibs.datastructure.*
import korlibs.datastructure.iterators.*
import korlibs.io.lang.*
import korlibs.time.* 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> = private val _battlers: MutableList<Battler> =
initializer.battlers.map { Battler(it, initializer.timestamp) }.toMutableList() initializer.battlers.map { Battler(it, initializer.timestamp) }.toMutableList()
private val _pendingEffects: PriorityQueue<BattleEffect> = private val _pendingEffects: PriorityQueue<BattleEffect> =
@ -12,37 +15,70 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
private set private set
/** Full history entries needed for rewinding. Stored from oldest to newest. */ /** 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. */ /** Actions which will be executed at a later timestamp. */
private val _pendingActions: PriorityQueue<BattleAction> = PriorityQueue() private val _pendingActions: PriorityQueue<BattleAction> = PriorityQueue()
/** Abbreviated history entries used only for saving replay data. Stored from oldest to newest. */ /** Abbreviated history entries used only for saving replay data. Stored from oldest to newest. */
private val _pastHistory: Queue<BattleAction> = Queue() private val _pastHistory: Queue<BattleAction> = Queue()
val battlers: List<Battler.Readonly> = _battlers.map { it.asReadonly } val battlers: List<Battler.Readonly> get() = _battlers.map { it.asReadonly }
val pendingEffects: List<BattleEffect> = _pendingEffects.toList() val pendingEffects: List<BattleEffect> get() = _pendingEffects.toList()
val history: List<HistoryEntry> = _history.toList() val history: List<HistoryEntry> get() = _history.toList()
val pendingActions: List<BattleAction> = _pendingActions.toList() val pendingActions: List<BattleAction> get() = _pendingActions.toList()
val pastHistory: List<BattleAction> = _pastHistory.toList() val pastHistory: List<BattleAction> get() = _pastHistory.toList()
init { private val eventAPI: EventAPI by lazy { EventAPI(this) }
saveHistoryEntry(initializer)
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( class EventAPI(private val battle: Battle) {
actions.firstOrNull().run { val battlers: List<Battler.Readonly> get() = battle.battlers
when (this) {
is LoadAction -> this fun addBattler(battler: Battler.Saved) {
else -> LoadAction.ZERO 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) { fun removeBattler(id: Battler.Id) {
injectActions(actions.drop(1)) battle._battlers.fastIterateRemove { it.id == id }
} else {
injectActions(actions)
} }
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) { fun abbreviateHistoryUpTo(timestamp: TimeSpan) {
while (_history.isNotEmpty() && _history.first.timestamp <= timestamp) { while (_history.isNotEmpty() && _history.first.timestamp <= timestamp) {
val previous = _history.removeFirst() val previous = _history.removeFirst()
if (previous is ActionHistoryEntry) { if (previous is HistoryEntry.Action) {
_pastHistory.enqueue(previous.action) _pastHistory.enqueue(previous.action)
} }
} }
@ -97,7 +133,7 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
while (_history.isNotEmpty() && _history.last.timestamp > timestamp) { while (_history.isNotEmpty() && _history.last.timestamp > timestamp) {
val previous = _history.removeLast() val previous = _history.removeLast()
if (previous is ActionHistoryEntry) { if (previous is HistoryEntry.Action) {
_pendingActions.add(previous.action) _pendingActions.add(previous.action)
} }
} }
@ -153,7 +189,9 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
private fun applyEvent(event: TimedEvent) { private fun applyEvent(event: TimedEvent) {
advanceTimeTo(event.timestamp) advanceTimeTo(event.timestamp)
with(event) { execute() } with(event) { with(eventAPI) {
execute()
} }
saveHistoryEntry(event) saveHistoryEntry(event)
} }
@ -161,17 +199,12 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
val archivedBattlers = _battlers.map { it.asSaved } val archivedBattlers = _battlers.map { it.asSaved }
val archivedEffects = _pendingEffects.toList() val archivedEffects = _pendingEffects.toList()
_history.add(when (entry) { _history.add(when (entry) {
is LoadAction -> ActionHistoryEntry( is BattleAction -> HistoryEntry.Action(
action = entry,
battlers = entry.battlers,
pendingEffects = entry.pendingEffects,
)
is BattleAction -> ActionHistoryEntry(
action = entry, action = entry,
battlers = archivedBattlers, battlers = archivedBattlers,
pendingEffects = archivedEffects, pendingEffects = archivedEffects,
) )
is BattleEffect -> EffectHistoryEntry( is BattleEffect -> HistoryEntry.Effect(
effect = entry, effect = entry,
battlers = archivedBattlers, battlers = archivedBattlers,
pendingEffects = archivedEffects, pendingEffects = archivedEffects,
@ -183,7 +216,39 @@ class Battle(initializer: LoadAction = LoadAction.ZERO) {
currentTime = entry.timestamp currentTime = entry.timestamp
_pendingEffects.clear() _pendingEffects.clear()
_pendingEffects.addAll(entry.pendingEffects) _pendingEffects.addAll(entry.pendingEffects)
_battlers.clear() _battlers.retainAll { live -> entry.battlers.any { saved -> saved.id == live.id } }
_battlers.addAll(entry.battlers.map { Battler(it, entry.timestamp) }) _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.math.*
import korlibs.time.* import korlibs.time.*
import kotlinx.serialization.Serializable
import kotlin.jvm.* import kotlin.jvm.*
import kotlin.math.* import kotlin.math.*
@ -28,10 +29,12 @@ data class Battler (
private var lastEnergy: Double = _energy, private var lastEnergy: Double = _energy,
private var lastStamina: Double = _stamina, private var lastStamina: Double = _stamina,
) { ) {
@JvmInline @JvmInline @Serializable
value class Id(val v: Int) 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 val id: Id get() = battler.id
var name: String get() = battler.name var name: String get() = battler.name
set(v) { battler._name = v } set(v) { battler._name = v }
@ -57,6 +60,8 @@ data class Battler (
set(v) { battler._energyPausedUntil = v } set(v) { battler._energyPausedUntil = v }
val currentUpdate: TimeSpan get() = battler.currentUpdate val currentUpdate: TimeSpan get() = battler.currentUpdate
val asSaved: Saved get() = battler.asSaved
fun becomeShaken() { fun becomeShaken() {
composure = health.coerceAtMost(0.0) composure = health.coerceAtMost(0.0)
if (shaken) { if (shaken) {
@ -98,6 +103,10 @@ data class Battler (
stamina = (stamina - cost).coerceAtLeast(0.0) stamina = (stamina - cost).coerceAtLeast(0.0)
energy = (energy - 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 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) advanceTo(t)
block(Editable(this)) block(eventAPI)
lastUpdate = currentUpdate lastUpdate = currentUpdate
lastShaken = shaken lastShaken = shaken
@ -286,6 +295,7 @@ data class Battler (
lastStamina = saved.stamina, lastStamina = saved.stamina,
) )
@Serializable
data class Saved( data class Saved(
val id: Id, val id: Id,

@ -1,6 +1,7 @@
package model package model
import korlibs.time.* import korlibs.time.*
import kotlinx.serialization.*
sealed interface HistoryEntry: Comparable<HistoryEntry> { sealed interface HistoryEntry: Comparable<HistoryEntry> {
val timestamp: TimeSpan val timestamp: TimeSpan
@ -10,20 +11,36 @@ sealed interface HistoryEntry: Comparable<HistoryEntry> {
override fun compareTo(other: HistoryEntry): Int { override fun compareTo(other: HistoryEntry): Int {
return timestamp.compareTo(other.timestamp) return timestamp.compareTo(other.timestamp)
} }
}
data class ActionHistoryEntry( @Serializable
val action: BattleAction, @SerialName("load")
override val pendingEffects: List<BattleEffect>, data class Load(
override val battlers: List<Battler.Saved>, override val timestamp: TimeSpan,
): HistoryEntry { override val pendingEffects: List<BattleEffect>,
override val timestamp: TimeSpan get() = action.timestamp override val battlers: List<Battler.Saved>,
} ): HistoryEntry {
companion object{
val ZERO = Load(timestamp = TimeSpan.ZERO, battlers = listOf(), pendingEffects = listOf())
}
}
data class EffectHistoryEntry( @Serializable
val effect: BattleEffect, @SerialName("histAction")
override val pendingEffects: List<BattleEffect>, data class Action(
override val battlers: List<Battler.Saved>, val action: BattleAction,
): HistoryEntry { override val pendingEffects: List<BattleEffect>,
override val timestamp: TimeSpan get() = effect.timestamp 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 package model
import korlibs.time.* import korlibs.time.*
import kotlinx.serialization.*
sealed interface TimedEvent: Comparable<TimedEvent> { @Serializable
val timestamp: TimeSpan sealed class TimedEvent: Comparable<TimedEvent> {
fun Battle.execute() abstract val timestamp: TimeSpan
abstract fun Battle.EventAPI.execute()
override fun compareTo(other: TimedEvent): Int { override fun compareTo(other: TimedEvent): Int {
return timestamp.compareTo(other.timestamp) 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 package scenes
import korlibs.event.*
import korlibs.korge.input.*
import korlibs.korge.scene.* import korlibs.korge.scene.*
import korlibs.korge.view.* import korlibs.korge.view.*
import korlibs.time.* import korlibs.time.*
@ -8,13 +10,13 @@ import views.*
import kotlin.math.* import kotlin.math.*
class BattleScene : Scene() { class BattleScene : Scene() {
private val battle = Battle(LoadAction( private val battle = Battle(HistoryEntry.Load(
timestamp = TimeSpan.ZERO, timestamp = TimeSpan.ZERO,
battlers = listOf( battlers = listOf(
Battler.Saved( Battler.Saved(
id = Battler.Id(0), id = Battler.Id(0),
name = "Us", name = "Us",
composure = 0.0, composure = 150.0,
health = 150.0, health = 150.0,
maxHealth = 200.0, maxHealth = 200.0,
energy = 0.0, energy = 0.0,
@ -29,7 +31,7 @@ class BattleScene : Scene() {
Battler.Saved( Battler.Saved(
id = Battler.Id(1), id = Battler.Id(1),
name = "Them", name = "Them",
composure = 0.0, composure = 150.0,
health = 150.0, health = 150.0,
maxHealth = 200.0, maxHealth = 200.0,
energy = 0.0, energy = 0.0,
@ -64,6 +66,30 @@ class BattleScene : Scene() {
time += TimeSpan(whole) time += TimeSpan(whole)
battle.setTimeTo(time) 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() { override suspend fun SContainer.sceneMain() {

Loading…
Cancel
Save