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()
}
}