diff --git a/src/commonMain/kotlin/model/Battle.kt b/src/commonMain/kotlin/model/Battle.kt index 2133760..c73e4a4 100644 --- a/src/commonMain/kotlin/model/Battle.kt +++ b/src/commonMain/kotlin/model/Battle.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 = initializer.battlers.map { Battler(it, initializer.timestamp) }.toMutableList() private val _pendingEffects: PriorityQueue = @@ -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 = Deque() + private val _history: Deque = Deque().also { it.add(initializer) } /** 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() + val battlers: List get() = _battlers.map { it.asReadonly } + val pendingEffects: List get() = _pendingEffects.toList() + val history: List get() = _history.toList() + val pendingActions: List get() = _pendingActions.toList() + val pastHistory: List get() = _pastHistory.toList() - init { - saveHistoryEntry(initializer) + private val eventAPI: EventAPI by lazy { EventAPI(this) } + + constructor(initializer: HistoryEntry.Load, actions: List, timestamp: TimeSpan? = null): + this(initializer) { + injectActions(actions) + if (timestamp != null) { + setTimeTo(timestamp) + } } - constructor(actions: List, timestamp: TimeSpan? = null): this( - actions.firstOrNull().run { - when (this) { - is LoadAction -> this - else -> LoadAction.ZERO + class EventAPI(private val battle: Battle) { + val battlers: List 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) { + 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, + ) } diff --git a/src/commonMain/kotlin/model/BattleAction.kt b/src/commonMain/kotlin/model/BattleAction.kt new file mode 100644 index 0000000..9a9fe2a --- /dev/null +++ b/src/commonMain/kotlin/model/BattleAction.kt @@ -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)) + } + } +} diff --git a/src/commonMain/kotlin/model/BattleEffect.kt b/src/commonMain/kotlin/model/BattleEffect.kt new file mode 100644 index 0000000..08031f1 --- /dev/null +++ b/src/commonMain/kotlin/model/BattleEffect.kt @@ -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) + } + } +} diff --git a/src/commonMain/kotlin/model/Battler.kt b/src/commonMain/kotlin/model/Battler.kt index d0abc4e..611d04c 100644 --- a/src/commonMain/kotlin/model/Battler.kt +++ b/src/commonMain/kotlin/model/Battler.kt @@ -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, diff --git a/src/commonMain/kotlin/model/HistoryEntry.kt b/src/commonMain/kotlin/model/HistoryEntry.kt index 9b5c2dd..2cb8e2a 100644 --- a/src/commonMain/kotlin/model/HistoryEntry.kt +++ b/src/commonMain/kotlin/model/HistoryEntry.kt @@ -1,6 +1,7 @@ package model import korlibs.time.* +import kotlinx.serialization.* sealed interface HistoryEntry: Comparable { val timestamp: TimeSpan @@ -10,20 +11,36 @@ sealed interface HistoryEntry: Comparable { 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 -} + @Serializable + @SerialName("load") + data class Load( + override val timestamp: TimeSpan, + override val pendingEffects: List, + override val battlers: List, + ): HistoryEntry { + companion object{ + val ZERO = Load(timestamp = TimeSpan.ZERO, battlers = listOf(), pendingEffects = listOf()) + } + } -data class EffectHistoryEntry( - val effect: BattleEffect, - override val pendingEffects: List, - override val battlers: List, -): HistoryEntry { - override val timestamp: TimeSpan get() = effect.timestamp + @Serializable + @SerialName("histAction") + data class Action( + val action: BattleAction, + override val pendingEffects: List, + override val battlers: List, + ): HistoryEntry { + override val timestamp: TimeSpan get() = action.timestamp + } + + @Serializable + @SerialName("histFX") + data class Effect( + 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 index ab80a8e..fb81692 100644 --- a/src/commonMain/kotlin/model/TimedEvent.kt +++ b/src/commonMain/kotlin/model/TimedEvent.kt @@ -1,31 +1,14 @@ package model import korlibs.time.* +import kotlinx.serialization.* -sealed interface TimedEvent: Comparable { - val timestamp: TimeSpan - fun Battle.execute() +@Serializable +sealed class TimedEvent: Comparable { + 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, - 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 c5478b3..fa3b643 100644 --- a/src/commonMain/kotlin/scenes/BattleScene.kt +++ b/src/commonMain/kotlin/scenes/BattleScene.kt @@ -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() {