Exit Full View

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

package uk.co.nickthecoder.gamescupboard.client.view

import com.soywiz.klock.seconds
import com.soywiz.korge.input.mouse
import com.soywiz.korge.tween.get
import com.soywiz.korge.view.*
import com.soywiz.korim.color.RGBA
import com.soywiz.korma.geom.Point
import com.soywiz.korma.geom.XY
import uk.co.nickthecoder.gamescupboard.client.*
import uk.co.nickthecoder.gamescupboard.common.Player
import uk.co.nickthecoder.gamescupboard.common.playingAreaHeight
import uk.co.nickthecoder.gamescupboard.common.playingAreaWidth
import kotlin.math.max
import kotlin.math.min


// Offsets for the speech bubbles
private const val avatarMarginX = 10
private const val avatarMarginY = 10

private const val defaultWidth = 100.0
private const val defaultHeight = 40.0

private const val borderSize = 2.0


class AvatarView(

    id: Int,
    val player: Player,
    bgColor: RGBA,

    /**
     * 0 = Top (Chat below)
     * 1 = Right (Chat left)
     * 2 = Bottom (Chat above)
     * 3 = Left (Chat right)
     */
    val side: Int = 0

) : GameObjectView(id) {

    private val border = if (player.id == localPlayerId()) {
        RoundRect(
            defaultWidth + borderSize * 2,
            defaultHeight + borderSize * 2,
            rx = 6.0 + borderSize,
            fill = avatarBorderColor
        ).apply {
            position(- width / 2, - height / 2)
            this@AvatarView.addChild(this)
        }
    } else {
        null
    }

    private val background = roundRect(defaultWidth, defaultHeight, rx = 6.0, fill = bgColor) {
        position(- width / 2, - height / 2)
    }

    private val textView = text(player.name, color = avatarTextColor) {
        position(- width / 2, - height / 2)
    }

    init {
        this.mouse.click.add { onClick() }
    }

    var playerName: String = player.name
        set(v) {
            field = v
            updateText()
        }

    /**
     * A count of the number of pieces this player has moved into their "private" area.
     *
     * In card games, this is the number of cards in hand.
     * In Scrabble, it is the number of tiles in their rack.
     */
    var privatePieceCount = 0
        set(v) {
            field = v
            updateText()
        }

    private fun updateText() {
        with(textView) {
            val count = privatePieceCount
            text = if (count == 0) {
                playerName
            } else {
                "$playerName ($count)"
            }
            position(- width / 2, - height / 2)
        }
    }

    private fun onClick() {
        gamesCupboardClient.chatInput.chatTo(player)
    }

    override fun isTouching(point: Point): Boolean {
        return point.x >= x - background.width / 2 && point.x <= x + background.width / 2 &&
            point.y >= y - background.height / 2 && point.y <= y + background.height / 2
    }

    private fun bubblePosition(bubble: Container): XY {
        val main = background

        val local = when (side) {
            0 -> Point(0.0, main.height / 2 + bubble.height / 2 + avatarMarginY) // Below
            1 -> Point(- main.width / 2 - bubble.width / 2 - avatarMarginX, 0.0) // Right
            2 -> Point(0.0, - main.height / 2 - bubble.height / 2 - avatarMarginY) // Bottom
            else -> Point(main.width / 2 + bubble.width / 2 + avatarMarginX, 0.0) // Left
        }

        return Point(
            min(playingAreaWidth - bubble.width / 2, max(bubble.width / 2, local.x + x)),
            min(playingAreaHeight - bubble.height / 2, max(bubble.height / 2, local.y + y))
        )
    }

    /**
     * The bubble will be positioned based on the avatar's position.
     * If it's near the top edge, then the bubble is below etc.
     * The offset is based on the "main" part of the Avatar.
     * Currently, this is the [background], but when/if we have graphical avatars,
     * then it will be the image.
     */
    fun speak(message: String, toAvatar: AvatarView? = null) {
        val bubble = gamesCupboardClient.playingArea.speechBubble(message)
        bubble.position(bubblePosition(bubble))

        // Fade into and out of view. Then remove the bubble.
        bubble.launchTween(
            bubble::alpha[0.0, 1.0], time = bubbleDuration, easing = OnFinishEase(bubbleEase) {
                this@AvatarView.removeChild(bubble)
            }
        )
        if (toAvatar != null) {
            val to = toAvatar.bubblePosition(bubble)
            bubble.launchTween(
                bubble::x[to.x], bubble::y[to.y], time = 0.5.seconds
            )
        }
    }

}