Exit Full View

Pill Popper / src / main / kotlin / uk / co / nickthecoder / pillpopper / Ghost.kt

package uk.co.nickthecoder.pillpopper

import uk.co.nickthecoder.tickle.Role
import uk.co.nickthecoder.tickle.action.*
import uk.co.nickthecoder.tickle.graphics.Color
import uk.co.nickthecoder.tickle.stage.findRoles
import uk.co.nickthecoder.tickle.util.Attribute
import uk.co.nickthecoder.tickle.util.CostumeAttribute

abstract class Ghost : Traveller() {

    /**
     * Initial delay at the start of the scene. Each Ghost will have a different delay, so they don't end up on
     * top of each other when travelling at the same speed.
     */
    @CostumeAttribute
    var initialIdle = 0

    @Attribute
    var highSpeed = 4.0

    @Attribute
    var lowSpeed = 3.0

    /**
     * Time to stay in the pen at the beginning of the scene (measured in whole-block movements)
     */
    @Attribute
    var exitAfter = 5

    /**
     * Time to stay in the pen after being eaten (measured in whole-block movements)
     */
    @Attribute
    var reExitAfter = 5

    /**
     * Each ghost has their own personality. Winky heads directly to the player, Stinky heads to where the player
     * is heading etc.
     */
    abstract val chaseScorer: (Direction) -> Double

    /**
     * A chasing movement of one block
     */
    val chaseOne = Do { changeDirection(chaseScorer) }
            .then(CheckTouching().whilst(MoveForwards()))

    /**
     * Chases forever - [movement] uses this most of the time.
     */
    val chase: Action = Do { canBeScared = true }.then(chaseOne.forever())

    /**
     * A run-away movement of one block
     */
    val runOne = Do { changeDirection { runAwayScorer(it) } }
            .then(CheckTouching().whilst(MoveForwards()))

    /**
     * Set when the eaten ghost has gone through the door. Reset when the delay is over, and the ghost is
     * seeking the door to escape.
     */
    var waitingInPen = true

    /**
     * Used to alter the test for canMove, to only allow access to a door when exiting or entering the pen.
     * Not when chasing, or running away. Also used to prevent ghosts turning scared while in the pen.
     */
    var seekingDoor: Boolean = false

    /**
     * Set to true when chasing the player.
     * Set to false when eaten
     */
    var canBeScared: Boolean = false

    /**
     * True after a power pill has been eaten. Reset when the pill was worn off, or I've been eaten
     */
    var scared: Boolean = false

    /**
     * true when the ghost has been eaten. Reset when out of the pen.
     * Used when touching the player (touching does nothing when eaten).
     */
    var eaten: Boolean = false

    var powerPillWarningAction: Action? = null
        set(v) {
            field = v
            v?.begin()
        }

    lateinit var door: Role

    override fun activated() {
        super.activated()

        speed = highSpeed
        actor.color = Color(1f, 1f, 1f, 0.8f)
        waitingInPen = true
        movement = Idle(initialIdle)
                .then(chaseOne.repeat(exitAfter))
                .then { waitingInPen = false }.then(seekDoorAction(afterAction = chase))

        val foundDoor = closest(actor.stage!!.findRoles<Door>())
        if (foundDoor == null) {
            actor.die()
            println("ERROR. No door found!")
        } else {
            door = foundDoor
        }
    }

    override fun tick() {
        super.tick()
        powerPillWarningAction?.act()
    }

    /**
     * After moving a whole block, decide which way to move next.
     * If nextMovement is set, then this different pattern of movement is started (by setting movement).
     */
    fun changeDirection(scorer: (Direction) -> Double) {
        val next = nextMovement
        if (next != null) {
            nextMovement = null
            movement = next
            movement.act()
            return
        }

        // For debugging only
        if (block !== findBlock()) {
            println("ERROR. Ghost is in the wrong block $block vs ${findBlock()} $actor")
        }

        if (block.hasInstance<Tunnel>()) {
            speed = lowSpeed
            enterTunnel()
        } else {
            direction = chooseDirection(scorer)
        }
    }

    fun chooseDirection(scorer: (Direction) -> Double): Direction {

        var bestScore = -Double.MAX_VALUE
        var bestDirection: Direction? = null

        Direction.values().forEach { dir ->
            if (dir != Direction.NONE && canMove(dir) && !dir.isOpposite(direction)) {
                val score = scorer(dir)
                if (score > bestScore) {
                    bestScore = score
                    bestDirection = dir
                }
            }
        }
        if (bestDirection == null) {
            // A dead end - turn around!
            return direction.opposite()
        } else {
            return bestDirection!!
        }
    }

    fun canMove(dir: Direction): Boolean {
        val neighbour = block.neighbour(dir)
        val isSolid = neighbour?.hasInstance<Solid>() != false

        if (isSolid) {
            if (seekingDoor) {
                return neighbour?.hasInstance<Door>() == true
            } else {
                return false
            }
        } else {
            return true
        }
    }

    /**
     * Run away from the player
     */
    fun runAwayScorer(dir: Direction): Double {
        return -scoreDirectlyTo(dir, Player.instance.actor.x, Player.instance.actor.y)
    }

    /**
     * Head for x,y
     */
    fun scoreDirectlyTo(dir: Direction, targetX: Double, targetY: Double): Double {
        return if (dir.dx == 0) {
            (targetY - actor.y) * dir.dy
        } else {
            (targetX - actor.x) * dir.dx
        }
    }

    /**
     * A power pill has been eaten by the Player.
     */
    fun runAway() {
        if (canBeScared) {
            actor.event("scared")
            speed = lowSpeed
            scared = true

            nextMovement = runOne.until { !eaten && !scared }
                    .then {
                        actor.event("default")
                        movement = chase
                    }
        }
    }

    /**
     * Caught by Player after a power pill has been eaten.
     */
    fun eaten() {
        actor.event("eaten") // Change appearance
        speed = highSpeed
        eaten = true
        scared = false
        canBeScared = false
        powerPillWarningAction = null

        val points = actor.createChild("points")
        points.textAppearance?.text = PillPopper.instance.eatenGhost().toString()

        val inPen = chaseOne.repeat(2)
                // Change back to a normal ghost
                .then {
                    waitingInPen = true
                    eaten = false
                    actor.event("default")
                }
                .then(
                        chaseOne.repeat(reExitAfter) // Wait in the pen for a while,
                                // Head out the door and resume chasing the player
                                .then { waitingInPen = false }
                                .then(seekDoorAction(afterAction = chase))
                )

        nextMovement = seekDoorAction(inPen)
    }

    /**
     * Avoid the player, until PillPopper tells us to chase after him again.
     */
    fun playerDied() {
        if (!eaten && !scared && !seekingDoor && !waitingInPen) {
            nextMovement = runOne.whilst(Delay(RESTART_PERIOD))
                    .then { nextMovement = chase }.then(runOne)
        }
    }

    fun seekDoorAction(afterAction: Action): Action {

        val scorer = { dir: Direction -> scoreDirectlyTo(dir, door.actor.x, door.actor.y) }

        return Do { seekingDoor = true }
                .then { changeDirection(scorer) }
                .then(MoveForwards())
                .then {
                    if (block.hasInstance<Door>()) {
                        seekingDoor = false
                        movement = afterAction
                    }
                }.forever()
    }

    fun touchingPlayer(): Boolean {
        val dx = Math.abs(Player.instance.actor.x - actor.x)
        val dy = Math.abs(Player.instance.actor.y - actor.y)

        return dx < TOUCHING_DISTANCE && dy < TOUCHING_DISTANCE
    }

    fun levelComplete() {
        movement = NoAction()
    }

    fun powerPillWarning() {
        if (scared) {
            val flash = Do { actor.event("default") }
                    .then(Idle(6))
                    .then { actor.event("scared") }
                    .then(Idle(20))

            powerPillWarningAction = flash.until { !scared }.then { powerPillWarningAction = null }
        }
    }

    fun powerPillEnd() {
        scared = false
    }

    inner class CheckTouching : Action {
        override fun act(): Boolean {

            if (touchingPlayer()) {
                if (eaten) {
                    // Do nothing
                } else if (scared) {
                    eaten()
                } else {
                    Player.instance.killed()
                }
            }
            return true
        }
    }
}