last update before radzathan destruction

main
Mari 4 months ago
parent 0c2b8446d4
commit ef495fd7dc
  1. 21
      build.gradle.kts
  2. 25
      src/commonMain/kotlin/model/BattleAction.kt
  3. 17
      src/commonMain/kotlin/model/BattleEffect.kt
  4. 46
      src/commonMain/kotlin/model/HistoryEntry.kt
  5. 14
      src/commonMain/kotlin/model/TimedEvent.kt
  6. 129
      src/commonMain/kotlin/net/deliciousreya/predkemon/battle/Battle.kt
  7. 95
      src/commonMain/kotlin/net/deliciousreya/predkemon/battle/BattleAction.kt
  8. 25
      src/commonMain/kotlin/net/deliciousreya/predkemon/battle/BattleEffect.kt
  9. 15
      src/commonMain/kotlin/net/deliciousreya/predkemon/battle/BattleEvent.kt
  10. 47
      src/commonMain/kotlin/net/deliciousreya/predkemon/battle/BattleHistoryEntry.kt
  11. 173
      src/commonMain/kotlin/net/deliciousreya/predkemon/battle/BattleTime.kt
  12. 182
      src/commonMain/kotlin/net/deliciousreya/predkemon/battle/Battler.kt
  13. 13
      src/commonMain/kotlin/net/deliciousreya/predkemon/client/Connection.kt
  14. 57
      src/commonMain/kotlin/net/deliciousreya/predkemon/client/WebSocketConnection.kt
  15. 4
      src/commonMain/kotlin/net/deliciousreya/predkemon/client/main.kt
  16. 23
      src/commonMain/kotlin/net/deliciousreya/predkemon/math/SplitMix32Random.kt
  17. 94
      src/commonMain/kotlin/net/deliciousreya/predkemon/math/Xoshiro128StarStarRandom.kt
  18. 7
      src/commonMain/kotlin/net/deliciousreya/predkemon/message/BattleActionMessage.kt
  19. 7
      src/commonMain/kotlin/net/deliciousreya/predkemon/message/BattleActionsMessage.kt
  20. 7
      src/commonMain/kotlin/net/deliciousreya/predkemon/message/LoadBattleMessage.kt
  21. 7
      src/commonMain/kotlin/net/deliciousreya/predkemon/message/Message.kt
  22. 12
      src/commonMain/kotlin/net/deliciousreya/predkemon/message/Player.kt
  23. 16
      src/commonMain/kotlin/net/deliciousreya/predkemon/message/QuitMessage.kt
  24. 84
      src/commonMain/kotlin/net/deliciousreya/predkemon/scenes/BattleScene.kt
  25. 64
      src/commonMain/kotlin/net/deliciousreya/predkemon/scenes/ConnectScene.kt
  26. 38
      src/commonMain/kotlin/net/deliciousreya/predkemon/views/BattlerView.kt
  27. 102
      src/commonMain/kotlin/scenes/BattleScene.kt
  28. 108
      src/jvmMain/kotlin/server/GameServer.kt
  29. 12
      src/jvmMain/kotlin/server/ServerPlayer.kt
  30. 49
      src/jvmMain/kotlin/server/WebSocketPlayer.kt
  31. 32
      src/jvmMain/kotlin/server/main.kt

@ -7,23 +7,16 @@ plugins {
korge {
id = "net.deliciousreya.predkemon"
// To enable all targets at once
//targetAll()
// To enable targets based on properties/environment variables
//targetDefault()
// To selectively enable targets
targetAll()
serializationJson()
targetJvm()
targetJs()
targetDesktop()
targetIos()
targetAndroid()
gameCategory = GameCategory.ROLE_PLAYING
serializationJson()
entryPoint = "net.deliciousreya.predkemon.client.MainKt.main"
jvmMainClassName = "net.deliciousreya.predkemon.client.MainKt"
entrypoint("server", "server.MainKt")
dependencyMulti("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0-RC")
dependencyMulti("io.github.quillraven.fleks:Fleks:2.5", registerPlugin = false)
}

@ -1,25 +0,0 @@
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))
}
}
}

@ -1,17 +0,0 @@
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)
}
}
}

@ -1,46 +0,0 @@
package model
import korlibs.time.*
import kotlinx.serialization.*
sealed interface HistoryEntry: Comparable<HistoryEntry> {
val timestamp: TimeSpan
val pendingEffects: List<BattleEffect>
val battlers: List<Battler.Saved>
override fun compareTo(other: HistoryEntry): Int {
return timestamp.compareTo(other.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())
}
}
@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,14 +0,0 @@
package model
import korlibs.time.*
import kotlinx.serialization.*
@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)
}
}

@ -1,21 +1,24 @@
package model
package net.deliciousreya.predkemon.battle
import korlibs.datastructure.*
import korlibs.datastructure.iterators.*
import korlibs.io.lang.*
import korlibs.time.*
import kotlinx.serialization.Serializable
import net.deliciousreya.predkemon.math.*
import kotlin.random.*
class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
class Battle(private val initializer: BattleHistoryEntry.Load) {
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 val random: Xoshiro128StarStarRandom =
Xoshiro128StarStarRandom(initializer.randomState)
var currentTime: BattleTime = 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) }
private val _history: Deque<BattleHistoryEntry> = Deque<BattleHistoryEntry>().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. */
@ -23,13 +26,14 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
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 history: List<BattleHistoryEntry> 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):
private val eventAPI: EventAPI by lazy { EventWritable(this) }
constructor(initializer: BattleHistoryEntry.Load, actions: List<BattleAction>, timestamp: BattleTime? = null):
this(initializer) {
injectActions(actions)
if (timestamp != null) {
@ -37,49 +41,73 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
}
}
class EventAPI(private val battle: Battle) {
val battlers: List<Battler.Readonly> get() = battle.battlers
interface EventAPI {
val battlers: List<Battler.Readonly>
val random: Random
fun addBattler(battler: Battler.Saved)
fun removeBattler(id: Battler.Id)
fun battlers(block: Battler.EventAPI.() -> Unit)
fun battler(id: Battler.Id): Battler.Readonly?
fun battler(id: Battler.Id, block: Battler.EventAPI.() -> Unit)
fun Battler.Readonly.update(block: Battler.EventAPI.() -> Unit)
fun Battler.Readonly.remove()
fun injectEffect(effect: BattleEffect)
fun injectEffects(effects: Iterable<BattleEffect>)
fun reseedRandom(seed: Xoshiro128StarStarRandom.State)
}
private class EventWritable(private val battle: Battle) : EventAPI {
override val battlers: List<Battler.Readonly> get() = battle.battlers
fun addBattler(battler: Battler.Saved) {
override 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) {
override fun removeBattler(id: Battler.Id) {
battle._battlers.fastIterateRemove { it.id == id }
}
fun battlers(block: Battler.EventAPI.() -> Unit) {
override 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
override fun battler(id: Battler.Id): Battler.Readonly? = battle._battlers.find { it.id == id }?.asReadonly
fun battler(id: Battler.Id, block: Battler.EventAPI.() -> Unit) {
override 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) {
override fun Battler.Readonly.update(block: Battler.EventAPI.() -> Unit) {
battler(id, block)
}
fun Battler.Readonly.remove() {
override fun Battler.Readonly.remove() {
removeBattler(id)
}
fun injectEffect(effect: BattleEffect) {
override fun injectEffect(effect: BattleEffect) {
when {
effect.timestamp > battle.currentTime -> battle._pendingEffects.add(effect)
effect.timestamp == battle.currentTime -> with (effect) { with (battle.eventAPI) {
execute()
} }
else -> throw InvalidArgumentException(
"can't inject an effect that would take place now or in the past: $effect")
"can't inject an effect that would take place in the past: $effect")
}
}
fun injectEffects(effects: Iterable<BattleEffect>) {
override fun injectEffects(effects: Iterable<BattleEffect>) {
effects.forEach { injectEffect(it) }
}
override val random: Random get() = battle.random
override fun reseedRandom(seed: Xoshiro128StarStarRandom.State) {
battle.random.loadState(seed)
}
}
fun injectAction(action: BattleAction) {
@ -100,16 +128,16 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
}
/** Abbreviates [_history] up to and including the given [timestamp], moving it to [_pastHistory]. */
fun abbreviateHistoryUpTo(timestamp: TimeSpan) {
fun abbreviateHistoryUpTo(timestamp: BattleTime) {
while (_history.isNotEmpty() && _history.first.timestamp <= timestamp) {
val previous = _history.removeFirst()
if (previous is HistoryEntry.Action) {
if (previous is BattleHistoryEntry.Action) {
_pastHistory.enqueue(previous.action)
}
}
}
fun setTimeTo(timestamp: TimeSpan) {
fun setTimeTo(timestamp: BattleTime) {
if (timestamp == currentTime) {
// then we're done already!
} else if (timestamp > currentTime){
@ -119,21 +147,21 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
}
}
private fun rewindTo(timestamp: TimeSpan) {
private fun rewindTo(timestamp: BattleTime) {
if (_history.isEmpty()) {
throw IllegalStateException(
"tried to rewind to ${timestamp.inWholeMilliseconds}ms, but have no full history at all")
"tried to rewind to $timestamp, 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}")
"tried to rewind to $timestamp, " +
"but only have full history back to ${_history.first.timestamp}")
}
while (_history.isNotEmpty() && _history.last.timestamp > timestamp) {
val previous = _history.removeLast()
if (previous is HistoryEntry.Action) {
if (previous is BattleHistoryEntry.Action) {
_pendingActions.add(previous.action)
}
}
@ -144,21 +172,22 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
}
}
private fun advanceTo(timestamp: TimeSpan) {
private fun advanceTo(timestamp: BattleTime) {
while (nextEventAt < timestamp) {
val nextEvent = if (nextActionAt < nextEffectAt) {
_pendingActions.removeHead()
} else {
_pendingEffects.removeHead()
}
advanceTimeTo(nextEvent.timestamp)
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 nextActionAt: BattleTime get() = nextAction?.timestamp ?: BattleTime.INFINITE
val nextEffectAt: BattleTime get() = nextEffect?.timestamp ?: BattleTime.INFINITE
val nextEventAt: BattleTime get() = nextEvent?.timestamp ?: BattleTime.INFINITE
val nextAction: BattleAction? get() = when {
_pendingActions.isNotEmpty() -> _pendingActions.head
@ -170,7 +199,7 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
else -> null
}
val nextEvent: TimedEvent? get() {
val nextEvent: BattleEvent? get() {
val action = nextAction
val effect = nextEffect
return when {
@ -179,7 +208,7 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
}
}
private fun advanceTimeTo(timestamp: TimeSpan) {
private fun advanceTimeTo(timestamp: BattleTime) {
if (currentTime == timestamp) {
return
}
@ -187,32 +216,34 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
_battlers.forEach { it.advanceTo(currentTime) }
}
private fun applyEvent(event: TimedEvent) {
advanceTimeTo(event.timestamp)
private fun applyEvent(event: BattleEvent) {
with(event) { with(eventAPI) {
execute()
} }
saveHistoryEntry(event)
}
private fun saveHistoryEntry(entry: TimedEvent) {
private fun saveHistoryEntry(entry: BattleEvent) {
val archivedBattlers = _battlers.map { it.asSaved }
val archivedEffects = _pendingEffects.toList()
val archivedRandom = random.copyState()
_history.add(when (entry) {
is BattleAction -> HistoryEntry.Action(
is BattleAction -> BattleHistoryEntry.Action(
action = entry,
battlers = archivedBattlers,
pendingEffects = archivedEffects,
randomState = archivedRandom,
)
is BattleEffect -> HistoryEntry.Effect(
is BattleEffect -> BattleHistoryEntry.Effect(
effect = entry,
battlers = archivedBattlers,
pendingEffects = archivedEffects,
randomState = archivedRandom,
)
})
}
private fun applyHistoryEntry(entry: HistoryEntry) {
private fun applyHistoryEntry(entry: BattleHistoryEntry) {
currentTime = entry.timestamp
_pendingEffects.clear()
_pendingEffects.addAll(entry.pendingEffects)
@ -226,15 +257,25 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
}
}
}
random.loadState(entry.randomState)
}
val asSaved: Saved get() = Saved(
val asLoadEntry: BattleHistoryEntry.Load
get() = BattleHistoryEntry.Load(
timestamp = BattleTime.ZERO,
pendingEffects = _pendingEffects.map { it.withTimestamp(it.timestamp - currentTime) },
battlers = _battlers.map { it.asSaved.withDelta(-currentTime) },
randomState = random.copyState()
)
val asSaved: Saved
get() = Saved(
currentTime = currentTime,
initializer = initializer,
history = buildList {
addAll(pastHistory)
_history.forEach {
if (it is HistoryEntry.Action) {
if (it is BattleHistoryEntry.Action) {
add(it.action)
}
}
@ -247,8 +288,8 @@ class Battle(val initializer: HistoryEntry.Load = HistoryEntry.Load.ZERO) {
@Serializable
data class Saved(
val currentTime: TimeSpan,
val initializer: HistoryEntry.Load,
val currentTime: BattleTime,
val initializer: BattleHistoryEntry.Load,
val history: List<BattleAction>,
)
}

@ -0,0 +1,95 @@
package net.deliciousreya.predkemon.battle
import kotlinx.serialization.Serializable
import net.deliciousreya.predkemon.math.*
@Serializable
/** A [BattleAction] represents some kind of input from a player. */
sealed class BattleAction: BattleEvent() {
abstract override fun withTimestamp(time: BattleTime): BattleAction
@Serializable
data class MultiHit(
override val timestamp: BattleTime,
val target: Battler.Id,
val hits: Int,
val firstHitDelay: BattleTime,
val subsequentHitDelay: BattleTime,
val damageRange: ClosedRange<Double>
): BattleAction() {
override fun Battle.EventAPI.execute() {
injectEffect(BattleEffect.Damage(
timestamp = timestamp + firstHitDelay,
id = target,
damage = random.nextDouble(damageRange.start, damageRange.endInclusive))
)
for (hit in 2..hits) {
injectEffect(BattleEffect.Damage(
timestamp = timestamp + firstHitDelay + subsequentHitDelay * (hit - 1),
id = target,
damage = random.nextDouble(damageRange.start, damageRange.endInclusive)))
}
}
override fun withTimestamp(time: BattleTime): MultiHit {
return copy(timestamp = time)
}
}
data class AddBattler(
override val timestamp: BattleTime,
val battler: Battler.Saved,
): BattleAction() {
override fun Battle.EventAPI.execute() {
addBattler(battler)
}
override fun withTimestamp(time: BattleTime): AddBattler {
return copy(timestamp = time)
}
}
data class RemoveBattler(
override val timestamp: BattleTime,
val battler: Battler.Id,
): BattleAction() {
override fun Battle.EventAPI.execute() {
removeBattler(battler)
}
override fun withTimestamp(time: BattleTime): RemoveBattler {
return copy(timestamp = time)
}
}
@Serializable
data class Reseed(
override val timestamp: BattleTime,
val newSeed: Xoshiro128StarStarRandom.State
): BattleAction() {
override fun Battle.EventAPI.execute() {
reseedRandom(newSeed)
}
override fun withTimestamp(time: BattleTime): Reseed {
return copy(timestamp = time)
}
}
@Serializable
data class Speak(
override val timestamp: BattleTime,
val speaker: Battler.Id,
val message: String,
) : BattleAction() {
override fun Battle.EventAPI.execute() {
battler(speaker) {
speak(message)
}
}
override fun withTimestamp(time: BattleTime): Speak {
return copy(timestamp = time)
}
}
}

@ -0,0 +1,25 @@
package net.deliciousreya.predkemon.battle
import kotlinx.serialization.Serializable
@Serializable
/** A [BattleEffect] represents some kind of timed impact on the game as a result of an [BattleAction]. */
sealed class BattleEffect: BattleEvent() {
abstract override fun withTimestamp(time: BattleTime): BattleEffect
@Serializable
data class Damage(
override val timestamp: BattleTime,
val id: Battler.Id,
val damage: Double): BattleEffect() {
override fun Battle.EventAPI.execute() {
battler(id) {
inflictComposureDamage(damage)
}
}
override fun withTimestamp(time: BattleTime): Damage {
return copy(timestamp = time)
}
}
}

@ -0,0 +1,15 @@
package net.deliciousreya.predkemon.battle
import kotlinx.serialization.Serializable
@Serializable
sealed class BattleEvent: Comparable<BattleEvent> {
abstract val timestamp: BattleTime
abstract fun Battle.EventAPI.execute()
abstract fun withTimestamp(time: BattleTime): BattleEvent
override fun compareTo(other: BattleEvent): Int {
return timestamp.compareTo(other.timestamp)
}
}

@ -0,0 +1,47 @@
package net.deliciousreya.predkemon.battle
import kotlinx.serialization.*
import net.deliciousreya.predkemon.math.*
@Serializable
sealed class BattleHistoryEntry: Comparable<BattleHistoryEntry> {
abstract val timestamp: BattleTime
abstract val pendingEffects: List<BattleEffect>
abstract val battlers: List<Battler.Saved>
abstract val randomState: Xoshiro128StarStarRandom.State
override fun compareTo(other: BattleHistoryEntry): Int {
return timestamp.compareTo(other.timestamp)
}
@Serializable
@SerialName("histLoad")
data class Load(
override val timestamp: BattleTime,
override val pendingEffects: List<BattleEffect>,
override val battlers: List<Battler.Saved>,
override val randomState: Xoshiro128StarStarRandom.State,
): BattleHistoryEntry()
@Serializable
@SerialName("histAction")
data class Action(
val action: BattleAction,
override val pendingEffects: List<BattleEffect>,
override val battlers: List<Battler.Saved>,
override val randomState: Xoshiro128StarStarRandom.State,
): BattleHistoryEntry() {
override val timestamp: BattleTime get() = action.timestamp
}
@Serializable
@SerialName("histFX")
data class Effect(
val effect: BattleEffect,
override val pendingEffects: List<BattleEffect>,
override val battlers: List<Battler.Saved>,
override val randomState: Xoshiro128StarStarRandom.State,
): BattleHistoryEntry() {
override val timestamp: BattleTime get() = effect.timestamp
}
}

@ -0,0 +1,173 @@
package net.deliciousreya.predkemon.battle
import korlibs.io.lang.*
import korlibs.math.*
import korlibs.time.*
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline
import kotlin.math.*
import kotlin.time.*
@JvmInline
@Serializable
value class BattleTime private constructor (val asMilliseconds: Int) : Comparable<BattleTime> {
constructor(
milliseconds: Int = 0,
seconds: Int = 0,
minutes: Int = 0,
hours: Int = 0,
days: Int = 0,
) : this(
days * DAY.asMilliseconds
+ hours * HOUR.asMilliseconds
+ minutes * MINUTE.asMilliseconds
+ seconds * SECOND.asMilliseconds
+ milliseconds
) {
if (this.asMilliseconds !in MIN.asMilliseconds..MAX.asMilliseconds) {
throw OutOfRangeException()
}
}
private constructor(millisecondsDouble: Double) : this(milliseconds = millisecondsDouble.toIntRound())
constructor(
milliseconds: Double = 0.0,
seconds: Double = 0.0,
minutes: Double = 0.0,
hours: Double = 0.0,
days: Double = 0.0,
) : this(
millisecondsDouble =
days * DAY.asMilliseconds
+ hours * HOUR.asMilliseconds
+ minutes * MINUTE.asMilliseconds
+ seconds * SECOND.asMilliseconds
+ milliseconds
)
operator fun plus(other: BattleTime): BattleTime {
return when (this) {
INFINITE -> when (other) {
NEGATIVE_INFINITE -> throw OppositeInfinitiesException()
else -> INFINITE
}
NEGATIVE_INFINITE -> when(other) {
INFINITE -> throw OppositeInfinitiesException()
else -> NEGATIVE_INFINITE
}
else -> BattleTime(asMilliseconds = asMilliseconds + other.asMilliseconds)
}
}
operator fun minus(other: BattleTime): BattleTime {
return when (this) {
INFINITE -> when (other) {
INFINITE -> throw OppositeInfinitiesException()
else -> INFINITE
}
NEGATIVE_INFINITE -> when(other) {
NEGATIVE_INFINITE -> throw OppositeInfinitiesException()
else -> NEGATIVE_INFINITE
}
else -> BattleTime(asMilliseconds = asMilliseconds - other.asMilliseconds)
}
}
operator fun times(other: Int): BattleTime {
return BattleTime(asMilliseconds * other)
}
operator fun times(other: Double): BattleTime {
return BattleTime(asMilliseconds * other)
}
operator fun div(other: Int): BattleTime {
return BattleTime(asMilliseconds / other)
}
operator fun div(other: Double): BattleTime {
return BattleTime(asMilliseconds / other)
}
operator fun div(other: BattleTime): Int {
return asMilliseconds / other.asMilliseconds
}
val days: Int get() = this.asMilliseconds / DAY.asMilliseconds
val asWholeDays: Int get() = days
val asDays: Double get() = this.asMilliseconds.toDouble() / DAY.asMilliseconds
val hours: Int get() = this.asMilliseconds.rem(DAY.asMilliseconds) / HOUR.asMilliseconds
val asWholeHours: Int get() = this.asMilliseconds / HOUR.asMilliseconds
val asHours: Double get() = this.asMilliseconds.toDouble() / HOUR.asMilliseconds
val minutes: Int get() = this.asMilliseconds.rem(HOUR.asMilliseconds) / MINUTE.asMilliseconds
val asWholeMinutes: Int get() = this.asMilliseconds / MINUTE.asMilliseconds
val asMinutes: Double get() = this.asMilliseconds.toDouble() / MINUTE.asMilliseconds
val seconds: Int get() = this.asMilliseconds.rem(MINUTE.asMilliseconds) / SECOND.asMilliseconds
val asWholeSeconds: Int get() = this.asMilliseconds / SECOND.asMilliseconds
val asSeconds: Double get() = this.asMilliseconds.toDouble() / SECOND.asMilliseconds
val milliseconds: Int get() = this.asMilliseconds.rem(SECOND.asMilliseconds)
fun isPositive(): Boolean {
return asMilliseconds > 0 && asMilliseconds <= MAX.asMilliseconds
}
override fun compareTo(other: BattleTime): Int {
return asMilliseconds.compareTo(other.asMilliseconds)
}
val absoluteValue get() = BattleTime(this.asMilliseconds.absoluteValue)
val asTimeSpan get() = TimeSpan(this.asMilliseconds.toDouble())
override fun toString(): String {
return when (this.absoluteValue) {
INFINITE -> "[BT::∞]"
NEGATIVE_INFINITE -> "[BT::-∞]"
in DAY..MAX -> "[BT::${days}d${hours.absoluteValue.toString().padStart(2, '0')}h${minutes.absoluteValue.toString().padStart(2, '0')}m${seconds.absoluteValue.toString().padStart(2, '0')}s${milliseconds.absoluteValue.toString().padStart(3, '0')}ms]"
in HOUR..<DAY -> "[BT::${hours}h${minutes.absoluteValue.toString().padStart(2, '0')}m${seconds.absoluteValue.toString().padStart(2, '0')}s${milliseconds.absoluteValue.toString().padStart(3, '0')}ms]"
in MINUTE..<HOUR -> "[BT::${minutes}m${seconds.absoluteValue.toString().padStart(2, '0')}s${milliseconds.absoluteValue.toString().padStart(3, '0')}ms]"
in SECOND..<MINUTE -> "[BT::${seconds}s${milliseconds.absoluteValue.toString().padStart(3, '0')}ms]"
else -> "[BT::${milliseconds}ms]"
}
}
operator fun unaryMinus(): BattleTime =
when (this) {
INFINITE -> NEGATIVE_INFINITE
NEGATIVE_INFINITE -> INFINITE
else -> BattleTime(-asMilliseconds)
}
class OppositeInfinitiesException : ArithmeticException("Can't add INFINITE and NEGATIVE_INFINITE")
class OutOfRangeException : ArithmeticException("Must be finite and in the range $MIN..$MAX")
companion object {
val ZERO = BattleTime(0)
val INFINITE = BattleTime(Int.MAX_VALUE)
val NEGATIVE_INFINITE = BattleTime(Int.MIN_VALUE)
val MIN = BattleTime(-24 * 24 * 60 * 60 * 1000) // Arbitrary limit of -24 days
val MAX = BattleTime(24 * 24 * 60 * 60 * 1000) // Arbitrary limit of 24 days
val MILLISECOND = BattleTime(1)
val SECOND = 1000 * MILLISECOND
val MINUTE = 60 * SECOND
val HOUR = 60 * MINUTE
val DAY = 24 * HOUR
val TimeSpan.asBattleTime: BattleTime get() =
BattleTime(milliseconds = this.millisecondsInt)
operator fun Int.times(other: BattleTime): BattleTime {
return BattleTime(this * other.asMilliseconds)
}
operator fun Double.times(other: BattleTime): BattleTime {
return BattleTime(this * other.asMilliseconds)
}
}
}

@ -1,7 +1,6 @@
package model
package net.deliciousreya.predkemon.battle
import korlibs.math.*
import korlibs.time.*
import kotlinx.serialization.Serializable
import kotlin.jvm.*
import kotlin.math.*
@ -19,50 +18,84 @@ data class Battler (
private var _shaken: Boolean = false,
private var _shakenTimes: Int = 0,
private var _composurePausedUntil: TimeSpan = TimeSpan.ZERO,
private var _energyPausedUntil: TimeSpan = TimeSpan.ZERO,
private var _currentUpdate: TimeSpan = TimeSpan.ZERO,
private var _composurePausedUntil: BattleTime = BattleTime.ZERO,
private var _energyPausedUntil: BattleTime = BattleTime.ZERO,
private var _currentUpdate: BattleTime = BattleTime.ZERO,
private var lastUpdate: TimeSpan = TimeSpan.ZERO,
private var _lastMessage: String? = null,
private var _lastMessageAt: BattleTime = BattleTime.NEGATIVE_INFINITE,
private var lastUpdate: BattleTime = BattleTime.ZERO,
private var lastShaken: Boolean = _shaken,
private var lastComposure: Double = _composure,
private var lastEnergy: Double = _energy,
private var lastStamina: Double = _stamina,
) {
@JvmInline @Serializable
value class Id(val v: Int)
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
value class Id(private val id: Int)
private val eventAPI: EventAPI by lazy { EventWritable(this) }
interface EventAPI {
val id: Id
var name: String
var composure: Double
var health: Double
var maxHealth: Double
var energy: Double
var stamina: Double
var maxStamina: Double
var shaken: Boolean
var shakenTimes: Int
var composurePausedUntil: BattleTime
var energyPausedUntil: BattleTime
val currentUpdate: BattleTime
val asSaved: Saved
fun speak(message: String)
fun becomeShaken()
fun inflictComposureDamage(damage: Double)
fun inflictHealthDamage(damage: Double)
fun spendEnergy(cost: Double)
fun spendStamina(cost: Double)
fun revertTo(saved: Saved)
}
class EventWritable (private val battler: Battler) : EventAPI {
override val id: Id get() = battler.id
override var name: String get() = battler.name
set(v) { battler._name = v }
var composure: Double get() = battler.composure
override var composure: Double get() = battler.composure
set(v) { battler._composure = v }
var health: Double get() = battler.health
override var health: Double get() = battler.health
set(v) { battler._health = v }
var maxHealth: Double get() = battler.maxHealth
override var maxHealth: Double get() = battler.maxHealth
set(v) { battler._maxHealth = v }
var energy: Double get() = battler.energy
override var energy: Double get() = battler.energy
set(v) { battler._energy = v }
var stamina: Double get() = battler.stamina
override var stamina: Double get() = battler.stamina
set(v) { battler._stamina = v }
var maxStamina: Double get() = battler.maxStamina
override var maxStamina: Double get() = battler.maxStamina
set(v) { battler._maxStamina = v }
var shaken: Boolean get() = battler.shaken
override var shaken: Boolean get() = battler.shaken
set(v) { battler._shaken = v }
var shakenTimes: Int get() = battler.shakenTimes
override var shakenTimes: Int get() = battler.shakenTimes
set(v) { battler._shakenTimes = v }
var composurePausedUntil: TimeSpan get() = battler.composurePausedUntil
override var composurePausedUntil: BattleTime
get() = battler.composurePausedUntil
set(v) { battler._composurePausedUntil = v }
var energyPausedUntil: TimeSpan get() = battler.energyPausedUntil
override var energyPausedUntil: BattleTime
get() = battler.energyPausedUntil
set(v) { battler._energyPausedUntil = v }
val currentUpdate: TimeSpan get() = battler.currentUpdate
override val currentUpdate: BattleTime get() = battler.currentUpdate
val asSaved: Saved get() = battler.asSaved
override val asSaved: Saved get() = battler.asSaved
fun becomeShaken() {
override fun speak(message: String) {
battler._lastMessage = message
battler._lastMessageAt = currentUpdate
}
override fun becomeShaken() {
composure = health.coerceAtMost(0.0)
if (shaken) {
return
@ -72,7 +105,7 @@ data class Battler (
shakenTimes += 1
}
fun inflictComposureDamage(damage: Double) {
override fun inflictComposureDamage(damage: Double) {
if (shaken) {
return
}
@ -84,7 +117,7 @@ data class Battler (
}
}
fun inflictHealthDamage(damage: Double) {
override fun inflictHealthDamage(damage: Double) {
composurePausedUntil = (currentUpdate + battler.composureRecoveryDelay).coerceAtLeast(composurePausedUntil)
health -= damage
composure -= damage
@ -93,18 +126,18 @@ data class Battler (
}
}
fun spendEnergy(cost: Double) {
override fun spendEnergy(cost: Double) {
energyPausedUntil = (currentUpdate + battler.energyChargeDelay).coerceAtLeast(energyPausedUntil)
energy = (energy - cost).coerceAtLeast(0.0)
}
fun spendStamina(cost: Double) {
override fun spendStamina(cost: Double) {
energyPausedUntil = (currentUpdate + battler.energyChargeDelay).coerceAtLeast(energyPausedUntil)
stamina = (stamina - cost).coerceAtLeast(0.0)
energy = (energy - cost).coerceAtLeast(0.0)
}
fun revertTo(saved: Saved) {
override fun revertTo(saved: Saved) {
battler.revertTo(previous = saved, at = currentUpdate)
}
}
@ -118,27 +151,29 @@ data class Battler (
val maxStamina: Double get() = _maxStamina
val shaken: Boolean get() = _shaken
val shakenTimes: Int get() = _shakenTimes
val composurePausedUntil: TimeSpan get() = _composurePausedUntil
val energyPausedUntil: TimeSpan get() = _energyPausedUntil
val currentUpdate: TimeSpan get() = _currentUpdate
val composurePausedUntil: BattleTime get() = _composurePausedUntil
val energyPausedUntil: BattleTime get() = _energyPausedUntil
val currentUpdate: BattleTime get() = _currentUpdate
val lastMessage: String? get() = _lastMessage
val lastMessageAt: BattleTime get() = _lastMessageAt
private val composureChargeTime: TimeSpan get() = TimeSpan(45_000.0 * 1.2.pow(shakenTimes - 1))
private val composureChargeDelay: TimeSpan get() = TimeSpan(2_000.0 * 1.2.pow(shakenTimes - 1))
private val composureRecoveryTime: TimeSpan get() = TimeSpan(7_000.0 * 1.2.pow(shakenTimes - 1))
private val composureRecoveryDelay: TimeSpan get() = TimeSpan(3_000.0 * 1.2.pow(shakenTimes - 1))
private val composureChargeTime: BattleTime get() = BattleTime(seconds = 45) * 1.2.pow(shakenTimes - 1)
private val composureChargeDelay: BattleTime get() = BattleTime(seconds = 2) * 1.2.pow(shakenTimes - 1)
private val composureRecoveryTime: BattleTime get() = BattleTime(seconds = 7) * 1.2.pow(shakenTimes - 1)
private val composureRecoveryDelay: BattleTime get() = BattleTime(seconds = 3) * 1.2.pow(shakenTimes - 1)
private val composureChargeCoefficient: Double get() = health / composureChargeTime.milliseconds.squared()
private val composureChargeCoefficient: Double get() = health / composureChargeTime.asMilliseconds.squared()
private val energyChargeTime: TimeSpan get() = TimeSpan(30_000.0)
private val energyChargeDelay: TimeSpan get() = TimeSpan(1_000.0)
private val staminaChargeTime: TimeSpan get() = TimeSpan(300_000.0)
private val energyChargeTime: BattleTime get() = BattleTime(seconds = 30)
private val energyChargeDelay: BattleTime get() = BattleTime(seconds = 1)
private val staminaChargeTime: BattleTime get() = BattleTime(minutes = 5)
private fun advanceComposureTo(t: TimeSpan) {
private fun advanceComposureTo(t: BattleTime) {
var dtComposure = if (lastUpdate < composurePausedUntil) {
if (t > composurePausedUntil) {
t - composurePausedUntil
} else {
TimeSpan.ZERO
BattleTime.ZERO
}
} else {
t - lastUpdate
@ -152,9 +187,9 @@ data class Battler (
dtComposure -= dtRecover
} else {
_composure = (
lastComposure.coerceAtLeast(0.0) + dtComposure.milliseconds * halfHealth / composureRecoveryTime.milliseconds)
lastComposure.coerceAtLeast(0.0) + dtComposure.asMilliseconds * halfHealth / composureRecoveryTime.asMilliseconds)
_shaken = lastShaken
dtComposure = TimeSpan.ZERO
dtComposure = BattleTime.ZERO
}
} else {
_composure = lastComposure
@ -162,13 +197,13 @@ data class Battler (
}
if (dtComposure.isPositive()) {
if (health > 0) {
val lastChargeTime = TimeSpan(
val lastChargeTime = BattleTime(
sqrt(4 * composureChargeCoefficient * composure) / (2 * composureChargeCoefficient)
)
_composure = if (dtComposure + lastChargeTime >= composureChargeTime) {
health
} else {
(composureChargeCoefficient * (lastChargeTime + dtComposure).milliseconds.squared())
(composureChargeCoefficient * (lastChargeTime + dtComposure).asMilliseconds.squared())
}
} else {
_composure = health
@ -176,12 +211,12 @@ data class Battler (
}
}
private fun advanceEnergyAndStaminaTo(t: TimeSpan) {
private fun advanceEnergyAndStaminaTo(t: BattleTime) {
var dtEnergy = if (lastUpdate < energyPausedUntil) {
if (t > energyPausedUntil) {
t - energyPausedUntil
} else {
TimeSpan.ZERO
BattleTime.ZERO
}
} else {
t - lastUpdate
@ -194,8 +229,8 @@ data class Battler (
_energy = lastStamina
dtEnergy -= dtToFull
} else {
_energy = lastEnergy + (lastStamina * dtEnergy.milliseconds) / energyChargeTime.milliseconds
dtEnergy = TimeSpan.ZERO
_energy = lastEnergy + (lastStamina * dtEnergy.asMilliseconds) / energyChargeTime.asMilliseconds
dtEnergy = BattleTime.ZERO
}
} else {
_energy = lastStamina
@ -204,13 +239,13 @@ data class Battler (
if (dtEnergy.isPositive() && lastStamina < maxStamina) {
_stamina = min(
maxStamina,
lastStamina + (maxStamina * dtEnergy.milliseconds) / staminaChargeTime.milliseconds)
lastStamina + (maxStamina * dtEnergy.asMilliseconds) / staminaChargeTime.asMilliseconds)
_energy = stamina
}
}
}
fun advanceTo(t: TimeSpan) {
fun advanceTo(t: BattleTime) {
_currentUpdate = t
if (composure < health) {
advanceComposureTo(t)
@ -220,7 +255,7 @@ data class Battler (
}
}
fun updateAt(t: TimeSpan, block: EventAPI.() -> Unit) {
fun updateAt(t: BattleTime, block: EventAPI.() -> Unit) {
advanceTo(t)
block(eventAPI)
@ -232,7 +267,8 @@ data class Battler (
lastStamina = stamina
}
val asSaved: Saved get() = Saved(
val asSaved: Saved
get() = Saved(
id = id,
name = name,
@ -246,10 +282,13 @@ data class Battler (
shaken = shaken,
shakenTimes = shakenTimes,
composurePausedUntil = composurePausedUntil,
energyPausedUntil = energyPausedUntil
energyPausedUntil = energyPausedUntil,
lastMessage = _lastMessage,
lastMessageAt = _lastMessageAt,
)
fun revertTo(previous: Saved, at: TimeSpan) {
fun revertTo(previous: Saved, at: BattleTime) {
_name = previous.name
_composure = previous.composure
_health = previous.health
@ -271,7 +310,7 @@ data class Battler (
lastStamina = previous.stamina
}
constructor(saved: Saved, at: TimeSpan) : this(
constructor(saved: Saved, at: BattleTime) : this(
id = saved.id,
_name = saved.name,
@ -287,6 +326,9 @@ data class Battler (
_composurePausedUntil = saved.composurePausedUntil,
_energyPausedUntil = saved.energyPausedUntil,
_lastMessage = saved.lastMessage,
_lastMessageAt = saved.lastMessageAt,
_currentUpdate = at,
lastUpdate = at,
lastShaken = saved.shaken,
@ -309,9 +351,19 @@ data class Battler (
val shaken: Boolean = false,
val shakenTimes: Int = 0,
val composurePausedUntil: TimeSpan = TimeSpan.ZERO,
val energyPausedUntil: TimeSpan = TimeSpan.ZERO,
val composurePausedUntil: BattleTime = BattleTime.ZERO,
val energyPausedUntil: BattleTime = BattleTime.ZERO,
val lastMessage: String?,
val lastMessageAt: BattleTime,
) {
fun withDelta(delta: BattleTime): Saved {
return copy(
composurePausedUntil = composurePausedUntil + delta,
energyPausedUntil = energyPausedUntil + delta,
lastMessageAt = lastMessageAt + delta,
)
}
}
val asReadonly get() = Readonly(this)
@ -326,9 +378,11 @@ data class Battler (
val maxStamina: Double get() = battler.maxStamina
val shaken: Boolean get() = battler.shaken
val shakenTimes: Int get() = battler.shakenTimes
val composurePausedUntil: TimeSpan get() = battler.composurePausedUntil
val energyPausedUntil: TimeSpan get() = battler.energyPausedUntil
val currentUpdate: TimeSpan get() = battler.currentUpdate
val composurePausedUntil: BattleTime get() = battler.composurePausedUntil
val energyPausedUntil: BattleTime get() = battler.energyPausedUntil
val currentUpdate: BattleTime get() = battler.currentUpdate
val asSaved: Saved get() = battler.asSaved
val lastMessage: String? get() = battler.lastMessage
val lastMessageAt: BattleTime get() = battler.lastMessageAt
}
}

@ -0,0 +1,13 @@
package net.deliciousreya.predkemon.client
import korlibs.io.async.*
import korlibs.io.lang.*
import net.deliciousreya.predkemon.message.*
interface Connection : Closeable {
val onLoad: Signal<LoadBattleMessage>
val onMessage: Signal<Message>
val onClose: Signal<Nothing?>
suspend fun sendMessage(message: Message)
}

@ -0,0 +1,57 @@
package net.deliciousreya.predkemon.client
import korlibs.io.async.*
import korlibs.io.lang.*
import korlibs.io.net.ws.*
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.*
import kotlinx.serialization.json.*
import net.deliciousreya.predkemon.message.*
import kotlin.coroutines.*
class WebSocketConnection(
private val client: WebSocketClient
) : Connection {
private val handlers: MutableList<Closeable> = mutableListOf()
private fun init() {
handlers.add(client.onOpen {
println("Connected to WebSocket")
})
handlers.add(client.onStringMessage {
when (val message = Json.decodeFromString(Message.serializer(), it)) {
is LoadBattleMessage -> onLoad(message)
else -> onMessage(message)
}
})
handlers.add(client.onClose {
onClose(null)
close()
})
handlers.add(client.onError {
it.printStackTrace()
})
}
override val onLoad: Signal<LoadBattleMessage> = Signal()
override val onMessage: Signal<Message> = Signal()
override val onClose: Signal<Nothing?> = Signal()
override suspend fun sendMessage(message: Message) {
client.send(Json.encodeToString(Message.serializer(), message))
}
override fun close() {
client.close()
onLoad.clear()
onMessage.clear()
onClose.clear()
handlers.forEach {
it.close()
}
}
companion object {
fun forClient(client: WebSocketClient): WebSocketConnection =
WebSocketConnection(client).also { it.init() }
}
}

@ -1,8 +1,10 @@
package net.deliciousreya.predkemon.client
import korlibs.image.color.*
import korlibs.korge.*
import korlibs.korge.scene.*
import korlibs.math.geom.*
import scenes.*
import net.deliciousreya.predkemon.scenes.*
suspend fun main() = Korge(windowSize = Size(512, 512), backgroundColor = Colors["#2b2b2b"]) {
val sceneContainer = sceneContainer()

@ -0,0 +1,23 @@
package net.deliciousreya.predkemon.math
import kotlin.random.*
class SplitMix32Random(seed: Int) : Random() {
var state: Int = seed
private set
override fun nextBits(bitCount: Int): Int {
state += -0x1e3779b9 // 0x9e3779b9
var z = state
z = z.xor(z.ushr(16))
z *= 0x21f0aaad
z = z.xor(z.ushr(15))
z *= 0x735a2d97
z = z.xor(z.ushr(15))
return z.ushr(32 - bitCount)
}
fun loadState(seed: Int) {
state = seed
}
}

@ -0,0 +1,94 @@
package net.deliciousreya.predkemon.math
import korlibs.datastructure.*
import korlibs.io.lang.*
import kotlinx.serialization.*
import kotlin.random.*
class Xoshiro128StarStarRandom(seed: State) : Random() {
private val state: State = seed.copy()
@Serializable
data class State(
var s0: Int,
var s1: Int,
var s2: Int,
var s3: Int,
) {
operator fun get(index: Int) =
when (index) {
0 -> s0
1 -> s1
2 -> s2
3 -> s3
else -> throw OutOfBoundsException(index, "State only has components 0-3")
}
operator fun set(index: Int, value: Int) {
when (index) {
0 -> s0 = value
1 -> s1 = value
2 -> s2 = value
3 -> s3 = value
else -> throw OutOfBoundsException(index, "State only has components 0-3")
}
}
fun updateFrom(other: State) {
s0 = other.s0
s1 = other.s1
s2 = other.s2
s3 = other.s3
}
companion object {
fun fromRandom(random: Random = Default): State {
return State(
s0 = random.nextInt(),
s1 = random.nextInt(),
s2 = random.nextInt(),
s3 = random.nextInt(),
)
}
fun fromSeed(seed: Int): State {
return fromRandom(SplitMix32Random(seed))
}
}
}
constructor(seed: Int) : this(State.fromSeed(seed))
constructor(random: Random) : this(State.fromRandom(random))
var totalSteps: Int = 0
private set
fun step(count: Int = 1) {
for (step in 1..count) {
val t = state[1].shl(9)
state[2] = state[2].xor(state[0])
state[3] = state[3].xor(state[1])
state[1] = state[1].xor(state[2])
state[0] = state[0].xor(state[3])
state[2] = state[2].xor(t)
state[3] = state[3].rotateLeft(11)
totalSteps += 1
}
}
override fun nextBits(bitCount: Int): Int {
val result = (state[1] * 5).rotateLeft(7) * 9
step(1)
return result.ushr(32 - bitCount)
}
fun copyState(): State = state.copy()
fun copyStateInto(other: State) {
other.updateFrom(state)
}
fun loadState(seed: State) {
state.updateFrom(seed)
}
}

@ -0,0 +1,7 @@
package net.deliciousreya.predkemon.message
import kotlinx.serialization.Serializable
import net.deliciousreya.predkemon.battle.BattleAction
@Serializable
data class BattleActionMessage(val action: BattleAction): Message()

@ -0,0 +1,7 @@
package net.deliciousreya.predkemon.message
import kotlinx.serialization.Serializable
import net.deliciousreya.predkemon.battle.BattleAction
@Serializable
data class BattleActionsMessage(val actions: List<BattleAction>): Message()

@ -0,0 +1,7 @@
package net.deliciousreya.predkemon.message
import kotlinx.serialization.Serializable
import net.deliciousreya.predkemon.battle.*
@Serializable
data class LoadBattleMessage (val saved: Battle.Saved): Message()

@ -0,0 +1,7 @@
package net.deliciousreya.predkemon.message
import kotlinx.serialization.Serializable
@Serializable
sealed class Message

@ -0,0 +1,12 @@
package net.deliciousreya.predkemon.message
import kotlinx.serialization.Serializable
import kotlin.jvm.*
@Serializable
data class Player(
val id: Id,
val name: String) {
@JvmInline @Serializable
value class Id(private val id: String)
}

@ -0,0 +1,16 @@
package net.deliciousreya.predkemon.message
import net.deliciousreya.predkemon.battle.*
data class QuitMessage(
val id: Player.Id,
val timestamp: BattleTime,
val reason: Reason,
) : Message() {
enum class Reason {
LEFT,
DISCONNECTED,
CRASHED,
}
}

@ -0,0 +1,84 @@
package net.deliciousreya.predkemon.scenes
import korlibs.event.*
import korlibs.korge.input.*
import korlibs.korge.scene.*
import korlibs.korge.view.*
import korlibs.time.*
import net.deliciousreya.predkemon.battle.*
import net.deliciousreya.predkemon.battle.BattleTime
import net.deliciousreya.predkemon.client.*
import net.deliciousreya.predkemon.message.*
import net.deliciousreya.predkemon.views.*
class BattleScene(message: LoadBattleMessage, connection: Connection, buffer: List<Message>) : Scene() {
private val battle = Battle(message.saved)
override suspend fun SContainer.sceneInit() {
val usView = BattlerView(battle.battlers[0])
usView.xy(sceneWidth - (usView.width + 5), sceneHeight - (usView.height + 5))
addChild(usView)
val themView = BattlerView(battle.battlers[1])
themView.xy(5, 5)
addChild(themView)
var time = BattleTime.ZERO
var nanos = 0
addUpdater {
val whole = it.millisecondsInt
nanos += it.rem(TimeSpan(milliseconds = 1.0)).nanosecondsInt
time += BattleTime(milliseconds = whole)
if (nanos > TimeSpan(milliseconds = 1.0).nanosecondsInt) {
time += BattleTime(milliseconds = nanos.floorDiv(TimeSpan(milliseconds = 1.0).nanosecondsInt))
nanos = nanos.rem(TimeSpan(milliseconds = 1.0).nanosecondsInt)
}
battle.setTimeTo(time)
}
keys {
justDown(Key.R) {
time = BattleTime.ZERO
nanos = 0
battle.setTimeTo(time)
}
justDown(Key.F) {
time += BattleTime(10_000.0)
battle.setTimeTo(time)
}
justDown(Key.M) {
battle.injectAction(
BattleAction.Speak(
timestamp = time + BattleTime(milliseconds = 100),
speaker = Battler.Id(0),
message = "Hello! It's ${time}!"
)
)
}
justDown(Key.V) {
battle.injectAction(
BattleAction.MultiHit(
timestamp = time,
target = Battler.Id(1),
hits = 5,
firstHitDelay = BattleTime(500.0),
subsequentHitDelay = BattleTime(100.0),
damageRange = 3.0..7.0
)
)
}
}
}
override suspend fun SContainer.sceneMain() {
// @TODO: Main scene code here (after sceneInit)
}
override suspend fun sceneAfterInit() {
super.sceneAfterInit()
}
}

@ -0,0 +1,64 @@
package net.deliciousreya.predkemon.scenes
import korlibs.event.*
import korlibs.io.net.ws.*
import korlibs.korge.input.*
import korlibs.korge.scene.*
import korlibs.korge.ui.*
import korlibs.korge.view.*
import kotlinx.coroutines.*
import net.deliciousreya.predkemon.client.*
import net.deliciousreya.predkemon.message.*
class ConnectScene(val sceneContainer: SceneContainer) : Scene() {
val textId = UIText("0")
val button = UIButton()
val status = UIText("Waiting for ID")
suspend fun attemptConnection() {
status.text = "Connecting"
val connection = WebSocketConnection.forClient(WebSocketClient(
url = "wss://localhost:8689/",
))
val deferred = CompletableDeferred<LoadBattleMessage>(null)
val buffer = mutableListOf<Message>()
val onLoad = connection.onLoad {
deferred.complete(it)
}
val onMessage = connection.onMessage {
buffer.add(it)
}
val onClose = connection.onClose {
@Suppress("ThrowableNotThrown")
deferred.completeExceptionally(
Exception("Closed by other end"))
}
val load: LoadBattleMessage? = try {
deferred.await()
} catch(ex: Throwable) {
status.text = "Failed: ${ex.message}"
ex.printStackTrace()
null
} finally {
onLoad.close()
onMessage.close()
onClose.close()
}
if (load != null) {
sceneContainer.changeTo {
BattleScene(load, connection, buffer)
}
}
}
override suspend fun SContainer.sceneInit() {
addChild(textId)
addChild(button)
addChild(status)
keys {
}
}
override suspend fun SContainer.sceneMain() {
// @TODO: Main scene code here (after sceneInit)
}
}

@ -1,14 +1,30 @@
package views
package net.deliciousreya.predkemon.views
import korlibs.image.color.*
import korlibs.image.paint.*
import korlibs.image.text.*
import korlibs.image.vector.*
import korlibs.korge.ui.*
import korlibs.korge.view.*
import korlibs.math.geom.*
import model.*
import net.deliciousreya.predkemon.battle.*
import kotlin.math.*
class BattlerView (val battler: Battler.Readonly) : UIContainer(Size(200.0, 40.0)) {
class BattlerView(private val battler: Battler.Readonly) : UIContainer(Size(400.0, 40.0)) {
companion object {
val chatVisibleOffset: BattleTime = BattleTime(seconds = 1)
val chatVisibleMsPerChar: BattleTime = BattleTime(milliseconds = 60)
fun chatVisibleTime(message: String?): BattleTime = when (message) {
null -> BattleTime.ZERO
else -> chatVisibleOffset + chatVisibleMsPerChar * message.length
}
fun showChat(message: String?, at: BattleTime, currentTime: BattleTime): Boolean = when (message) {
null -> false
else -> currentTime in at..(at + chatVisibleTime(message))
}
}
private val staminaBar = graphics {
drawStaminaBar()
@ -18,11 +34,16 @@ class BattlerView (val battler: Battler.Readonly) : UIContainer(Size(200.0, 40.0
}
private val staminaText = uiText("${battler.energy.roundToInt()}/${battler.stamina.roundToInt()}/${battler.maxStamina.roundToInt()}").xy(100, 25)
private val healthText = uiText("${battler.composure.roundToInt()}/${battler.health.roundToInt()}/${battler.maxHealth.roundToInt()}").xy(100, 5)
private val chatText = textBlock(
size = Size(200.0, 40.0),
align = TextAlignment.TOP_LEFT,
).xy(200.0, 0.0)
private val battlerName = uiText(battler.name)
private var composure: Double = battler.composure
private var health: Double = battler.health
private var energy: Double = battler.energy
private var stamina: Double = battler.stamina
private var chat = ""
init {
addUpdater {
@ -84,5 +105,16 @@ class BattlerView (val battler: Battler.Readonly) : UIContainer(Size(200.0, 40.0
stamina = battler.stamina
updateStaminaBar()
}
val shouldChatBeVisible = showChat(
message = battler.lastMessage,
at = battler.lastMessageAt,
currentTime = battler.currentUpdate
)
if (shouldChatBeVisible != chatText.visible || (shouldChatBeVisible && chat != battler.lastMessage)) {
chatText.visible = shouldChatBeVisible
if (shouldChatBeVisible) {
chatText.plainText = battler.lastMessage ?: ""
}
}
}
}

@ -1,102 +0,0 @@
package scenes
import korlibs.event.*
import korlibs.korge.input.*
import korlibs.korge.scene.*
import korlibs.korge.view.*
import korlibs.time.*
import model.*
import views.*
import kotlin.math.*
class BattleScene : Scene() {
private val battle = Battle(HistoryEntry.Load(
timestamp = TimeSpan.ZERO,
battlers = listOf(
Battler.Saved(
id = Battler.Id(0),
name = "Us",
composure = 150.0,
health = 150.0,
maxHealth = 200.0,
energy = 0.0,
stamina = 150.0,
maxStamina = 300.0,
shaken = false,
shakenTimes = 0,
composurePausedUntil = TimeSpan.ZERO,
energyPausedUntil = TimeSpan.ZERO,
),
Battler.Saved(
id = Battler.Id(1),
name = "Them",
composure = 150.0,
health = 150.0,
maxHealth = 200.0,
energy = 0.0,
stamina = 150.0,
maxStamina = 300.0,
shaken = false,
shakenTimes = 0,
composurePausedUntil = TimeSpan.ZERO,
energyPausedUntil = TimeSpan.ZERO,
)
),
pendingEffects = listOf()
))
override suspend fun SContainer.sceneInit() {
val usView = BattlerView(battle.battlers[0])
usView.xy(sceneWidth - (usView.width + 5), sceneHeight - (usView.height + 5))
addChild(usView)
val themView = BattlerView(battle.battlers[1])
themView.xy(5, 5)
addChild(themView)
var time = TimeSpan.ZERO
var fractional = 0.0
addUpdater {
val step = it.milliseconds + fractional
val whole = floor(step)
fractional = step - whole
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() {
// @TODO: Main scene code here (after sceneInit)
}
override suspend fun sceneAfterInit() {
super.sceneAfterInit()
}
}

@ -0,0 +1,108 @@
package server
import korlibs.io.async.*
import korlibs.io.lang.*
import korlibs.time.*
import net.deliciousreya.predkemon.battle.*
import net.deliciousreya.predkemon.battle.BattleTime.Companion.asBattleTime
import net.deliciousreya.predkemon.math.*
import net.deliciousreya.predkemon.message.*
import kotlin.coroutines.*
class GameServer {
private val _battle = Battle(BattleHistoryEntry.Load(
timestamp = BattleTime.ZERO,
pendingEffects = listOf(),
battlers = listOf(),
randomState = Xoshiro128StarStarRandom.State.fromRandom()
))
val battle get() = _battle.also { catchUp() }
private var startRelative: DateTime = DateTime.EPOCH
private var running: Boolean = false
private fun catchUp() {
if (running) {
_battle.setTimeTo((DateTime.now() - startRelative).asBattleTime)
}
}
fun resume() {
if (running) {
return
}
running = true
startRelative = DateTime.now() - _battle.currentTime.asTimeSpan
}
fun pause() {
if (!running) {
return
}
running = false
startRelative = DateTime.EPOCH
}
data class WrappedPlayer(
val player: ServerPlayer,
val onMessage: Closeable,
val onDisconnect: Closeable,
)
val players: MutableList<WrappedPlayer> = mutableListOf()
suspend fun handleMessage(source: ServerPlayer?, message: Message) {
when (message) {
is BattleActionMessage -> {
battle.injectAction(message.action)
broadcastMessage(source, message)
}
is BattleActionsMessage -> {
battle.injectActions(message.actions)
broadcastMessage(source, message)
}
is LoadBattleMessage -> {
// discard
}
is QuitMessage -> {
// discard
}
}
}
suspend fun broadcastMessage(source: ServerPlayer?, message: Message) {
players.forEach {
launch(coroutineContext) {
if (it.player != source) {
it.player.sendMessage(message)
}
}
}
}
suspend fun addPlayer(player: ServerPlayer) {
val onMessage = player.onMessage.addSuspend {
handleMessage(player, it)
}
val onDisconnect = player.onDisconnect.addSuspend {
removePlayer(player)
}
resume()
players.add(WrappedPlayer(
player = player,
onMessage = onMessage,
onDisconnect = onDisconnect,
))
player.sendMessage(LoadBattleMessage(battle.asSaved))
}
fun removePlayer(player: ServerPlayer) {
val wrapped = players.find { it.player == player }
wrapped?.onMessage?.close()
players.remove(wrapped)
if (players.isEmpty()) {
pause()
}
}
}

@ -0,0 +1,12 @@
package server
import korlibs.io.async.*
import korlibs.io.lang.*
import net.deliciousreya.predkemon.message.*
interface ServerPlayer : Closeable {
val onMessage: Signal<Message>
val onDisconnect: Signal<Nothing?>
suspend fun sendMessage(message: Message)
}

@ -0,0 +1,49 @@
package server
import korlibs.io.async.*
import korlibs.io.lang.*
import korlibs.io.net.http.*
import kotlinx.serialization.json.*
import net.deliciousreya.predkemon.message.*
class WebSocketPlayer private constructor(
private val request: HttpServer.WsRequest
) : ServerPlayer {
private val listeners: MutableList<Closeable> = mutableListOf()
private fun initialize() {
request.onClose {
onDisconnect(null)
close()
}
request.onStringMessage {
val msg = Json.decodeFromString(Message.serializer(), it)
onMessage(msg)
}
}
companion object {
fun forRequest(request: HttpServer.WsRequest): WebSocketPlayer {
val player = WebSocketPlayer(request)
player.initialize()
return player
}
}
override fun close() {
onMessage.clear()
onDisconnect.clear()
listeners.forEach { it.close() }
request.close()
}
override val onMessage: Signal<Message> = Signal()
override val onDisconnect: Signal<Nothing?> = Signal()
override suspend fun sendMessage(message: Message) {
request.send(
Json.encodeToString(
Message.serializer(), message))
}
}

@ -0,0 +1,32 @@
package server
import korlibs.io.lang.*
import korlibs.io.net.http.*
import kotlinx.coroutines.*
fun main() {
val server = createHttpServer()
val game = GameServer()
runBlocking {
server.websocketHandler {
println("new WS request!")
it.accept(Http.Headers.build {})
game.addPlayer(WebSocketPlayer.forRequest(it))
}
server.httpHandler {
println("new request!")
it.setStatus(200, "OK")
it.write("Running!", Charset.forName("UTF-8"))
it.end()
}
server.errorHandler {
println("oops failure")
it.printStackTrace()
}
server.listen(port = 8689, host = "localhost")
}
}
Loading…
Cancel
Save