parent
0c2b8446d4
commit
ef495fd7dc
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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,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…
Reference in new issue