You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

254 lines
8.9 KiB

package model
import korlibs.datastructure.*
import korlibs.datastructure.iterators.*
import korlibs.io.lang.*
import korlibs.time.*
import kotlinx.serialization.Serializable
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> =
PriorityQueue<BattleEffect>().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<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> 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()
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)
}
}
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))
}
fun removeBattler(id: Battler.Id) {
battle._battlers.fastIterateRemove { it.id == id }
}
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) }
}
}
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<BattleAction>) {
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 HistoryEntry.Action) {
_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 HistoryEntry.Action) {
_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) { with(eventAPI) {
execute()
} }
saveHistoryEntry(event)
}
private fun saveHistoryEntry(entry: TimedEvent) {
val archivedBattlers = _battlers.map { it.asSaved }
val archivedEffects = _pendingEffects.toList()
_history.add(when (entry) {
is BattleAction -> HistoryEntry.Action(
action = entry,
battlers = archivedBattlers,
pendingEffects = archivedEffects,
)
is BattleEffect -> HistoryEntry.Effect(
effect = entry,
battlers = archivedBattlers,
pendingEffects = archivedEffects,
)
})
}
private fun applyHistoryEntry(entry: HistoryEntry) {
currentTime = entry.timestamp
_pendingEffects.clear()
_pendingEffects.addAll(entry.pendingEffects)
_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>,
)
}