import uk.co.nickthecoder.glok.property.property
import uk.co.nickthecoder.glok.scene.Color
import uk.co.nickthecoder.kyd.Actor
import uk.co.nickthecoder.kyd.Enumerable
import uk.co.nickthecoder.kyd.Ticks
import uk.co.nickthecoder.kyd.appearance.AppearanceResource
import uk.co.nickthecoder.kyd.appearance.FlipBook
import uk.co.nickthecoder.kyd.appearance.flipBook
import uk.co.nickthecoder.kyd.appearance.page
import uk.co.nickthecoder.kyd.util.Custom
import uk.co.nickthecoder.kyd.util.CustomWarning
private const val EAST = 0
private const val NORTH = 1
private const val WEST = 2
private const val SOUTH = 3
/**
* Note, we need to implement [Enumerable] so that these values can be saved in .act files without using reflection
* (which isn't available in JS).
*/
enum class MonsterMovement : Enumerable<MonsterMovement> {
ATTACK_STICK_HORIZONTAL,
ATTACK_STICK_VERTICAL,
ATTACK_STICK_BOTH,
ATTACK_STICK_NONE,
;
override fun enumerateValues() = MonsterMovement.entries
}
class Monster : Item(), Killable {
private var idle = true
private var direction = NORTH
/**
* 0 : Attack, but get stuck horizontally (standard)
* 1 : Attack, but get stuck vertically (not horizontally) (alternate)
* 2 : Attack, but get stuck vertically and horizontally (easy)
* 3 : Attack and never get stuck (hard).
*/
@Suppress(CustomWarning)
@Custom
val movementTypeProperty by property(MonsterMovement.ATTACK_STICK_HORIZONTAL)
var movementType by movementTypeProperty
/**
* A monster can get stuck in horizontal tunnels.
* (They don't get stuck in vertical tunnels).
*/
var allowReverse = true
private lateinit var walkingAnimation: FlipBook
private lateinit var idleAnimation: FlipBook
override fun customProperties() = listOf( movementTypeProperty)
override fun onEnteredStage(actor: Actor) {
super.onEnteredStage(actor)
if (! CavernQuest.instance.monstersGetStuck) {
if (movementType == MonsterMovement.ATTACK_STICK_HORIZONTAL || movementType == MonsterMovement.ATTACK_STICK_VERTICAL || movementType == MonsterMovement.ATTACK_STICK_BOTH) {
movementType = MonsterMovement.ATTACK_STICK_NONE
}
}
val role = actor.role ?: return
val a = role.find("a") as AppearanceResource
val b = role.find("b") as AppearanceResource
idleAnimation = flipBook {
page(a, monsterIdleTime)
page(b, monsterIdleTime)
}
walkingAnimation = flipBook {
val pageCount = 2
repetitions = 1
page(a, monsterWalkTime / repetitions / pageCount)
page(b, monsterWalkTime / repetitions / pageCount)
onFinished {
idle = true
idleAnimation.applyToActor(actor)
}
}
idleAnimation.applyToActor(actor)
}
private fun oppositeDirection(direction: Int) = (direction + 2) % 4
private fun turnRight(direction: Int) = (direction - 1) % 4
private fun turnLeft(direction: Int) = (direction + 1) % 4
override fun tick(actor: Actor, seconds: Float) {
(actor.appearance as? Ticks)?.tick(actor, seconds)
if (idle) {
when (movementType) {
MonsterMovement.ATTACK_STICK_HORIZONTAL -> attack(actor)
MonsterMovement.ATTACK_STICK_VERTICAL -> attack(actor, stickHorizontal = false, stickVertical = true)
MonsterMovement.ATTACK_STICK_BOTH -> attack(actor, stickHorizontal = true, stickVertical = true)
MonsterMovement.ATTACK_STICK_NONE -> attack(actor, stickHorizontal = false, stickVertical = false)
}
}
}
/**
* This is my best-effort to reproduce the original behaviour of Cavern Quest monsters.
* I'm not sure if it is identical, but the game _feels_ the same to me.
* In particular the solutions that I used as a kid still work ;-)
*
* Note, the movement can be altered by supplying different values to [stickHorizontal] and [stickVertical].
* Setting them both the `true` will probably make the game too easy.
*/
private fun attack(actor: Actor, stickHorizontal: Boolean = true, stickVertical: Boolean = false) {
val player = PlayDirector.instance.player ?: return
val deltaX = player.gridX - gridX
val deltaY = player.gridY - gridY
fun headingAway(direction: Int): Boolean {
val (dx, dy) = directionToDeltaXY(direction)
if (dx * deltaX > 0) return false
if (dy * deltaY > 0) return false
return true
}
var newDirection: Int
if (deltaY == 0) {
// In the same column as player. Try to Head DIRECTLY towards them.
newDirection = if (deltaX > 0) EAST else WEST
if (stickHorizontal) {
// If we are in a horizontal tunnel, don't turn around. Get stuck!
allowReverse = ! (look(0, - 1).isWall() && look(0, 1).isWall())
}
} else if (deltaX == 0) {
// In the same row as the player. Try to Head DIRECTLY towards them.
newDirection = if (deltaY > 0) NORTH else SOUTH
if (stickVertical) {
// If we are in a vertical tunnel, don't turn around. Get stuck!
allowReverse = ! (look(- 1, 0).isWall() && look(1, 0).isWall())
}
} else {
// Diagonal to the player. Do not head in one direction; turn if possible.
// This lets the player be cunning, and isn't optimal for the monster!
newDirection = turnLeft(direction)
// If that means we are heading away, then try RIGHT first
if (headingAway(newDirection)) {
newDirection = oppositeDirection(newDirection)
}
if (! canMove(newDirection)) {
// Couldn't turn that way, try the other way.
newDirection = oppositeDirection(newDirection)
if (! canMove(newDirection)) {
// We can't turn left OR right. Try straight ahead
if (canMove(direction)) {
newDirection = direction
} else {
// We can't turn left right or travel straight ahead. Reversing is the only other option!
// But is this allowed?
if (allowReverse) {
newDirection = oppositeDirection(direction)
}
}
}
}
}
// We've picked a direction, but it might be blocked.
if (canMove(newDirection)) {
direction = newDirection
move(actor, direction)
// Whenever we successfully move, we can't be in a dead-end, so reversing IS allowed.
allowReverse = true
}
// debugColor(actor)
}
/**
* Helped during debugging.
*/
private fun debugColor(actor: Actor) {
actor.appearance.tintIfAvailable = if (allowReverse) Color.WHITE else Color["#dead"]
}
/**
* Oh dear, we got squashed by a rock. Turn into to soil,
* and spawn another monster at a random spawn point.
*/
override fun killedBy(killer: Actor, victim: Actor) {
}
private fun directionToDeltaXY(direction: Int): Pair<Int, Int> {
return when (direction) {
EAST -> Pair(1, 0)
WEST -> Pair(- 1, 0)
SOUTH -> Pair(0, - 1)
else -> Pair(0, 1)
}
}
private fun canMove(direction: Int): Boolean {
val (dx, dy) = directionToDeltaXY(direction)
val look = look(dx, dy)
return if (CavernQuest.instance.friendly) {
// On Easy mode, the monsters are friendly, they just want to follow you. Good doggie.
look.isEmpty() && look?.behaviour !is Player
} else {
look.isEmpty()
}
}
private fun move(actor: Actor, direction: Int) {
val (dx, dy) = directionToDeltaXY(direction)
val hit = look(dx, dy)
if (move(actor, dx, dy)) {
idle = false
walkingAnimation.applyToActor(actor)
(hit?.behaviour as? Killable)?.killedBy(actor, hit)
}
}
}