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 = 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().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 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() 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) } } 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)) } 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) { 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) { 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, ) }