Exit Full View

Cavern Quest 2 / src / commonMain / kotlin / PlayDirector.kt

import uk.co.nickthecoder.glok.event.Key
import uk.co.nickthecoder.glok.event.KeyEvent
import uk.co.nickthecoder.glok.property.Property
import uk.co.nickthecoder.glok.property.boilerplate.booleanProperty
import uk.co.nickthecoder.glok.property.boilerplate.intProperty
import uk.co.nickthecoder.kyd.Actor
import uk.co.nickthecoder.kyd.Director
import uk.co.nickthecoder.kyd.Management
import uk.co.nickthecoder.kyd.Role
import uk.co.nickthecoder.kyd.nodes.PlayActView
import uk.co.nickthecoder.kyd.util.*
import kotlin.math.min
import kotlin.random.Random

/**
 * Coordinates play.
 */
class PlayDirector : Director {

    @Suppress(CustomWarning)
    @Custom
    val blastsProperty by intProperty(99)
    var blasts by blastsProperty

    /**
     * When we collect [Fuel], should [Monster]s unstick themselves if they are "trapped" in dead-end?
     * This makes the level harder!
     */
    @Suppress(CustomWarning)
    @Custom
    val unstickMonstersProperty by booleanProperty(false)
    var unstickMonsters by unstickMonstersProperty

    /**
     * How close to the edge of the view does the Player need to be before scrolling to reveal
     * more of the world? Part of the skill is to plan your route without seeing what is yet to be discovered.
     * So these constants should be quite small.
     */
    private var scrollMarginX = gridSize * 4
    private var scrollMarginY = gridSize * 3

    val grid = Grid(gridSize)

    /**
     * Used by [Monster]s to home in on their prey.
     * Set by [Player] when it enters the stage using [registerPlayer].
     */
    var player: Player? = null
        private set

    var playerActor: Actor? = null
        private set

    /**
     * The main aim of the game is to collect fuel and then return to the ship to leave the planet.
     * Set by [Fuel]. Checked by [Player] when it attempts to enter the [Ship].
     */
    val fueledProperty by booleanProperty(false)
    var fueled by fueledProperty

    private val spawnPoints = mutableListOf<Vector2>()

    init {
        instance = this
    }

    override fun customProperties() = listOf(blastsProperty, unstickMonstersProperty)

    fun registerPlayer(actor: Actor, player: Player) {
        this.playerActor = actor
        this.player = player
        player.blastsProperty.bidirectionalBind(blastsProperty)
    }

    fun registerBlastsCounter(blastsCounter: BlastsCounter) {
        blastsCounter.blastsProperty.bidirectionalBind(blastsProperty)
    }

    fun registerFuelStatus(fuelStatus: FuelStatus) {
        fuelStatus.fueledProperty.bidirectionalBind(fueledProperty)
    }

    fun addSpawnPoint(point: Vector2) {
        spawnPoints.add(point)
    }

    fun collectedDynamite(amount: Int) {
        blasts = min(CavernQuest.instance.maxBlasts, blasts + amount * CavernQuest.instance.blastsPerDynamite)
    }

    fun collectedFuel() {
        fueled = true
        if (unstickMonsters) {
            unstickMonsters()
        }
    }

    /**
     * A nasty surprise, which I don't think is in the original.
     * When you collect fuel (or die, or take off), monsters. which are "stuck" in a dead-end,
     * are allowed to reverse direction.
     */
    private fun unstickMonsters() {
        val mainStage = PlayActView.instance.act?.stages?.firstOrNull { it.name == "main" } ?: return
        for (actor in mainStage.actors) {
            (actor.behaviour as? Monster)?.allowReverse = true
        }
    }

    fun blastOff(shipActor: Actor) {
        unstickMonsters()
    }

    fun playerDied() {
        unstickMonsters()
        // TODO Merge a "Game Over" act.
    }


    /**
     * Called by [Rock] when it squashes a [Monster].
     *
     * Pick one of the [spawnPoints] randomly. If it is empty create a new [Monster].
     * If it's not empty, try again up to 10 times. Then give up.
     */
    fun spawnNewMonster() {
        if (spawnPoints.isEmpty()) return
        val mainStage = PlayActView.instance.act?.stages?.firstOrNull { it.name == "main" } ?: return

        for (attempt in 0 until 10) {
            val index = Random.nextInt(spawnPoints.size)
            val point = spawnPoints[index]
            if (grid.getAt(point) == null) {
                val monster = PlayActView.instance.game.find("/roles/monster") as? Role
                if (monster == null) {
                    println("Monster Role not found. Not spawning")
                    return
                }
                val newActor = Actor(mainStage).apply {
                    behaviour = Monster()
                    role = monster
                    position = point
                }
                mainStage.addActor(newActor)
                return
            }
        }
    }

    override fun onKeyPressed(keyEvent: KeyEvent) {

        when (keyEvent.key) {
            Key.F5 -> Management.instance.reloadAct()
            Key.ESCAPE -> Management.instance.loadAct("/menu")
            Key.PRINT_SCREEN -> if (keyEvent.isControlDown) {
                createScreenshot("screenshot.png")
                createThumbnail("thumbnail.png")
            }
            else -> Unit
        }

    }

    /**
     * When the player gets neat an edge of the screen, scroll to reveal more of the world.
     */
    override fun postTick(seconds: Float) {

        val actView = PlayActView.instance

        playerActor?.let { actor ->

            val scenePosition = actor.position * actView.worldToScene
            val reqX = if (scenePosition.x < actView.sceneX + scrollMarginX) {
                actView.worldPin.x - gridSize
            } else if (scenePosition.x + scrollMarginX > actView.sceneX + actView.width) {
                actView.worldPin.x + gridSize
            } else {
                actView.worldPin.x
            }

            val reqY = if (fueled) {
                actor.position.y
            } else {
                if (scenePosition.y < actView.sceneY + scrollMarginY) {
                    actView.worldPin.y + gridSize
                } else if (scenePosition.y + scrollMarginY > actView.sceneY + actView.height) {
                    actView.worldPin.y - gridSize
                } else {
                    actView.worldPin.y
                }
            }
            if (fueled) {
                val speed = Vector2(0.1f, 0.02f)
                actView.worldPin = (actView.worldPin + Vector2(reqX, reqY) * speed) / (Vector2.ONE + speed)
            } else {
                actView.worldPin = Vector2(reqX, reqY)
            }

        }
    }

    companion object {
        lateinit var instance: PlayDirector
    }
}