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.image.color.* |
||||||
import korlibs.korge.* |
import korlibs.korge.* |
||||||
import korlibs.korge.scene.* |
import korlibs.korge.scene.* |
||||||
import korlibs.math.geom.* |
import korlibs.math.geom.* |
||||||
import scenes.* |
import net.deliciousreya.predkemon.scenes.* |
||||||
|
|
||||||
suspend fun main() = Korge(windowSize = Size(512, 512), backgroundColor = Colors["#2b2b2b"]) { |
suspend fun main() = Korge(windowSize = Size(512, 512), backgroundColor = Colors["#2b2b2b"]) { |
||||||
val sceneContainer = sceneContainer() |
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