Exit Full View

Games Cupboard / gamescupboard-client / src / commonMain / kotlin / uk / co / nickthecoder / gamescupboard / client / clientUtils.kt

package uk.co.nickthecoder.gamescupboard.client

import Message
import Packet
import RunCommand
import com.soywiz.klock.Date
import com.soywiz.klock.DateTime
import com.soywiz.klock.TimeSpan
import com.soywiz.klock.seconds
import com.soywiz.korau.sound.Sound
import com.soywiz.korau.sound.readSound
import com.soywiz.korge.tween.V2
import com.soywiz.korge.tween.tween
import com.soywiz.korge.view.View
import com.soywiz.korio.async.launch
import com.soywiz.korio.file.std.resourcesVfs
import com.soywiz.korma.interpolation.Easing
import io.ktor.websocket.*
import kotlinx.serialization.encodeToString
import uk.co.nickthecoder.gamescupboard.common.CommandPrototype

fun WebSocketSession.sendMessage(message: Message) {
    launch {
        send(json.encodeToString(Packet(message)))
    }
}

fun Message.send() {
    gamesCupboardClient.session.sendMessage(this)
}

/**
 * Processes the message locally and sends it to the server.
 */
fun Message.localAndSend() {
    gamesCupboardClient.session.launch {
        gamesCupboardClient.processMessage(this)
        gamesCupboardClient.session.send(json.encodeToString(Packet(this)))
    }
}


private val soundCache = mutableMapOf<String, Sound>()
private suspend fun loadAndCacheSound(path: String): Sound? {
    return try {
        val result = resourcesVfs[path].readSound()
        soundCache[path] = result
        result
    } catch (e: Exception) {
        println("Failed to load sound $path : $e")
        null
    }
}

private fun playSound(sound: Sound) {
    gamesCupboardClient.theStage.views.launch {
        sound.play()
    }
}

private var lastPlayedTime = 0.0
fun playSound(path: String) {
    // Only play one sound at a time. Other are discarded.
    val now = DateTime.nowUnixMillis()
    if (now < lastPlayedTime + 100) return
    lastPlayedTime = now

    soundCache[path]?.let {
        try {
            playSound(it)
        } catch (e: Exception) {
            println("Failed to play sound : $e")
        }
        return
    }
    // The sound isn't loaded...
    gamesCupboardClient.session.launch {
        loadAndCacheSound(path)?.let { playSound(it) }
    }
}

fun playTaunt(name: String) = playSound("/taunts/${name.trim().lowercase()}.mp3")

// Paths for various standard sound effects.
const val CANCEL_DRAG = "sounds/cancelDrag.mp3"
const val PLAYER_JOINED = "sounds/playerJoined.mp3"
const val PLAYER_LEFT = "sounds/playerLeft.mp3"
const val FACE_UP = "sounds/faceUp.mp3"
const val FACE_DOWN = "sounds/faceDown.mp3"

/**
 * When I need to tween, but not in a suitable suspend context.
 */
fun View.launchTween(
    vararg vs: V2<*>,
    time: TimeSpan = 1.seconds,
    easing: Easing = Easing.EASE_IN_OUT_QUAD,
    waitTime: TimeSpan = TimeSpan.NIL
) {
    stage?.launch {
        this.tween(*vs, time = time, easing = easing, waitTime = waitTime,
            timeout = false, autoInvalidate = true, callback = {}
        )
    }
}

fun View.launchTweenThen(
    vararg vs: V2<*>,
    time: TimeSpan = 1.seconds,
    easing: Easing = Easing.EASE_IN_OUT_QUAD,
    waitTime: TimeSpan = TimeSpan.NIL,
    then: () -> Unit
) {
    stage?.launch {
        this.tween(*vs, time = time, waitTime = waitTime,
            easing = OnFinishEase(easing, then),
            timeout = false, autoInvalidate = true, callback = {}
        )
    }
}

/**
 * An easing function, which returns to its original state, with a flat period of 1.0 in the middle.
 * i.e. ease(0) = 0 and ease(margin) .. ease(1-margin) == 1 and ease(1) = 0.
 */
class SymmetricFlatEase(
    private val source: Easing,
    private val margin: Double
) : Easing {
    override fun invoke(it: Double): Double {
        return if (it < margin) {
            source(it / margin)
        } else if (it > 1 - margin) {
            source((1 - it) / margin)
        } else {
            1.0
        }
    }
}

val bubbleEase = SymmetricFlatEase(Easing.EASE_OUT, 0.1)
val bubbleDuration = 5.seconds

/**
 * This is a "bodge", because tween does not have an "onFinished" callback.
 * (despite what the docs say, the callback is called at each step in the animation).
 * We cannot use the Double passed to that callback, because it is the "eased" value,
 * and therefore a "bounce" ease will pass 1.0 BEFORE the end of the animation.
 *
 * NOTE. Due to another "bug" in tween, [onFinish] would be called TWICE. Grr.
 * So [previous] is needed to prevent this.
 */
class OnFinishEase(val ease: Easing, val onFinish: () -> Unit) : Easing {
    private var previous = 0.0
    override fun invoke(it: Double): Double {
        if (it >= 1.0 && previous < 1.0) {
            onFinish()
        }
        previous = it
        return ease(it)
    }
}

/**
 * Either run the command (if [CommandPrototype.isComplete] == true),
 * otherwise, add the incomplete command to chat input.
 */
fun CommandPrototype.run() {
    if (isComplete) {
        RunCommand(commandName, parameters).send()
    } else {
        gamesCupboardClient.chatInput.commandPrototype(this)
    }
}