Exit Full View

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

package uk.co.nickthecoder.gamescupboard.client

import ChatMessage
import RunCommand
import com.soywiz.korev.Key
import com.soywiz.korev.KeyEvent
import com.soywiz.korge.input.keys
import com.soywiz.korge.service.storage.Storage
import com.soywiz.korge.view.Container
import com.soywiz.korge.view.addTo
import uk.co.nickthecoder.gamescupboard.client.text.TextInput
import uk.co.nickthecoder.gamescupboard.client.text.textInput
import uk.co.nickthecoder.gamescupboard.common.CommandPrototype
import uk.co.nickthecoder.gamescupboard.common.Player

fun Container.chatInput(): ChatInput {
    val ci = ChatInput()
    ci.addTo(this)
    return ci
}

class ChatInput : Container() {

    val history = mutableListOf<String>()
    var historyIndex = 0

    val textInput: TextInput = textInput(width = 500.0, skin = myBoxSkin).apply {
        controller.promptText = "Type here to chat. Type ? for help"
        controller.textColor = textColor
        controller.onReturnPressed.add { send() }
        keys.down { onKeyDown(it) }
        controller.onTextUpdated {
            onTextUpdated(text)
        }
    }

    fun focus() = textInput.focus()

    fun chatTo(player: Player) {
        textInput.text = "@${player.name}  ${textInput.text}"
        textInput.controller.cursorIndex = textInput.text.length
        focus()
    }

    private fun replaceText(text: String) {
        gamesCupboardClient.helpArea.clear()
        textInput.text = text
        textInput.controller.cursorIndex = textInput.text.length
        textInput.focus()
    }

    /**
     * Display "help" dependent on the current contents of the chat's text field.
     *
     * Help for commands is also absent if the client is a spectator.
     */
    private fun onTextUpdated(text: String) {
        val textTrimmed = text.trim()

        if (text == "?") {
            // Help on how to use the chat input...
            with(gamesCupboardClient.helpArea) {
                clear()
                addButton("Chat to 1 Player") {
                    replaceText("@")
                }
                if (!isSpectator()) {
                    addButton("Sounds") {
                        replaceText("!")
                    }
                }
                addButton("Commands") {
                    replaceText(":")
                }
                show()
            }

        } else if (textTrimmed.startsWith("@")) {
            // Chat to a single player

            val withoutAt = textTrimmed.substring(1)
            val space = withoutAt.indexOf(" ")

            val playerName = if (space < 0) withoutAt else withoutAt.substring(space)
            val player = gamesCupboardClient.findPlayer(playerName)

            with(gamesCupboardClient.helpArea) {
                clear()

                // List all players. Click to enter the player's name into the chat input.
                val otherPlayers = gamesCupboardClient.otherPlayerNames
                if (otherPlayers.isEmpty()) {
                    addLabel("There are no other players present")
                    show()
                    return
                } else {
                    addLabel("Chat to a single player")
                }
                for (name in otherPlayers) {
                    addButton(name) {
                        replaceText("@$name ")
                    }
                }
                // Spectators shouldn't be sending sound effects! They are distracting.
                if (!isSpectator()) {
                    addLabel("Sounds")
                    for (taunt in gamesCupboardClient.taunts) {
                        addButton(taunt) {
                            sendChat("!$taunt", player)
                            playTaunt(taunt)
                            replaceText("")
                        }
                    }
                }

                show()
            }

        } else if (textTrimmed.startsWith("!") && !isSpectator()) {
            // Play a taunt to all players

            with(gamesCupboardClient.helpArea) {
                clear()
                addLabel("Sounds")
                for (taunt in gamesCupboardClient.taunts) {
                    addButton(taunt) {
                        sendChat("!$taunt")
                        playTaunt(taunt)
                        replaceText("")
                    }
                }
                show()
            }
        } else if (textTrimmed.startsWith(":")) {
            // Run a command

            with(gamesCupboardClient.helpArea) {
                clear()
                if (isSpectator()) {
                    // Spectators can only send the :name command.
                    with(gamesCupboardClient.helpArea) {
                        addLabel("Commands : ")
                        addButton("name") {
                            replaceText(":name ")
                        }
                        show()
                    }
                } else {
                    val (commandName, parameters) = parseCommand(textTrimmed)
                    val commandInfo = gamesCupboardClient.commands.firstOrNull { it.name == commandName }
                    if (commandInfo == null) {
                        // List all command beginning with commandName.
                        with(gamesCupboardClient.helpArea) {
                            addLabel("Commands : ")
                            for (command in gamesCupboardClient.commands.filter { it.name.startsWith(commandName) }) {
                                addButton(command.name) {
                                    replaceText(":${command.name} ")
                                }
                            }
                            show()
                        }
                    } else {

                        // Help about the command
                        addLabel(":${commandInfo.name} : ${commandInfo.helpText}")
                        show()

                        // Help about the command's parameter
                        val paramIndex =
                            parameters.size + if (parameters.isNotEmpty() && !textTrimmed.endsWith(" ")) -1 else 0

                        if (paramIndex < commandInfo.parameterHelp.size) {
                            addLabel("Parameter #${paramIndex + 1} : ${commandInfo.parameterHelp[paramIndex]}")
                            show()
                        }

                    }
                }
            }
        } else {
            // Clear help
            gamesCupboardClient.helpArea.clear()
        }
    }

    private fun onKeyDown(e: KeyEvent) {
        if (e.key == Key.UP) {
            if (historyIndex > 0) {
                textInput.text = history[--historyIndex]
                textInput.controller.cursorIndex = textInput.text.length
            }
        }
        if (e.key == Key.DOWN) {
            if (historyIndex >= history.size - 1) {
                textInput.text = ""
                historyIndex = history.size
            } else {
                textInput.text = history[++historyIndex]
                textInput.controller.cursorIndex = textInput.text.length
            }
        }
    }

    fun send() {
        val text = textInput.text.trim()

        // A hidden "special" command, which
        if (text == ":FORGET") {
            Storage(gamesCupboardClient.theStage.views)["FORGET"] = "true"
            Storage(gamesCupboardClient.theStage.views).remove("playerName")
            return
        } else if (text == ":REMEMBER") {
            Storage(gamesCupboardClient.theStage.views).remove("FORGET")
        }

        history.add(text)
        historyIndex = history.size

        if (text.isNotEmpty()) {
            // Note, commands cannot be sent by spectators
            if (text.startsWith(':') && !isSpectator()) {
                // A Command, not a chat message.

                val (commandName, parameters) = parseCommand(text)
                RunCommand(commandName, parameters).send()

                // textInput is not cleared yet. It will be clear iff we receive a CommandOk reply.
                return

            } else if (text.startsWith('@')) {
                // Chat to a single player

                val space = text.indexOf(' ')
                if (space < 0) return
                val name = text.substring(1, space).trim()
                val message = text.substring(space + 1).trim()
                val player = gamesCupboardClient.findPlayer(name) ?: return
                sendChat(message, player)
            } else {
                // Chat to everyone
                sendChat(text)
            }
            textInput.text = ""

        }

    }

    private fun sendChat(text: String, to: Player? = null) {
        val chatMessage = ChatMessage(localPlayerId(), text, to?.id)
        chatMessage.localAndSend()
    }

    private fun parseCommand(text: String): Pair<String, List<String>> {
        val space = text.indexOf(' ')
        return if (space > 0) {
            Pair(text.substring(1, space), parseParameters(text.substring(space + 1)))
        } else {
            Pair(text.substring(1), emptyList())
        }
    }

    /**
     * Splits [str] into a list of strings. Split by spaces, but ignoring spaces
     * if a parameter is surrounded with single or double quotes.
     * e.g. ``Abc  "def ghi"  jkl`` will return 3 items.
     */
    private fun parseParameters(str: String): List<String> {
        var openQuote: Char? = null
        val result = mutableListOf<String>()
        var wordStart = 0
        for (i in str.indices) {
            val c = str[i]
            if (c == openQuote) {
                // End quoted text
                result.add(str.substring(wordStart, i))
                openQuote = null
            } else if (c == '"' || c == '\'' && i == wordStart) {
                openQuote = c
                wordStart++
            } else if (c == ' ' && openQuote == null) {
                if (i != wordStart) {
                    result.add(str.substring(wordStart, i))
                }
                wordStart = i + 1
            }
        }
        val remainder = str.substring(wordStart).trim()
        if (remainder.isNotBlank()) {
            result.add(remainder)
        }
        return result.filter { it.isNotBlank() }
    }

    fun commandPrototype(prototype: CommandPrototype) {
        textInput.text = prototype.parameters.joinToString(
            prefix = ":${prototype.commandName} ",
            separator = " ",
            postfix = " "
        )
        textInput.controller.cursorIndex = textInput.text.length
        focus()
    }

}