Code Sketch


horror game
By: Mhalsakant School
Category: Programming
import javax.swing._
import java.awt._
import java.awt.event._
import java.awt.geom._
import java.io._
import java.nio.file._
import scala.collection.mutable.ArrayBuffer

val GAME_W = 1440
val GAME_H = 860
val VIEW_H = 700
val MAP_W = 48
val MAP_H = 32
val FOV = Math.PI / 3.0
val HALF_FOV = FOV / 2.0
val RAYS = 360
val MAX_DEPTH = 38.0
val PI2 = Math.PI * 2.0
val PLAYER_RADIUS = 0.20
val WALK_SPEED = 0.050
val SPRINT_SPEED = 0.092
val STRAFE_SPEED = 0.045
val ROT_SPEED = 0.050
val MOUSE_SENS = 0.0065
val MAX_TILT = 0.34
val FLOOR_DETAIL = 24

val frame = new JFrame(
  "OLD HOUSE: NIGHT WATCH - 3D HORROR ESCAPE OMEGA V5"
)

frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.setSize(GAME_W, GAME_H)
frame.setResizable(false)
frame.setLocationRelativeTo(null)

var state = "MENU"
var difficulty = "NORMAL"
var paused = false
var muted = false
var showMap = true
var showHelp = false
var showBlueprint = false
var flashlightOn = true
var hidden = false
var running = true

var gameTime = 0.0
var bestTime = 0.0
var score = 0
var health = 100.0
var stamina = 100.0
var battery = 100.0
var fear = 0.0
var noiseLevel = 0.0

var message =
  "Find the house key. The front gate is deadlocked."

var messageTicks = 360
var animTick = 0L

// FIX: val -> var
var mouseDown = false
var mouseLastX = 0

var difficultyScale = 1.0
var enemySpeedScale = 1.0
var detectionScale = 1.0
var batteryDrain = 0.023
var lastSoundTick = -100L

var px = 4.5
var py = 27.5
var pa = -Math.PI / 2.0

var ex = 30.5
var ey = 4.5
var ea = Math.PI

var enemyState = "PATROL"
var enemyTargetX = 30.5
var enemyTargetY = 4.5
var enemyDistance = 99.0
var enemyVisible = false
var enemyStun = 0.0
var enemySearch = 0.0
var enemyWander = 0

var lastNoiseX = 6.0
var lastNoiseY = 26.0
var lastPlayerX = px
var lastPlayerY = py

var scareFlash = 0
var lightning = 0
var doorPulse = 0
var exitGlow = 0

var crouched = false
var headBob = 0.0
var cameraBobX = 0.0
var cameraBobY = 0.0
var footstepPulse = 0.0
var screenShake = 0.0
var interactionPulse = 0.0

var discoveredRooms =
  Array.fill[Boolean](20)(false)

var totalDiscoveries = 0
var flashlightFlicker = 0.0
var ambientPulse = 0.0
var doorAnim = 0.0
var enemyAlpha = 245
var interactMode = "NONE"
var cameraRoll = 0.0
var screenGrain = 0.0
var tutorialStep = 0
var hasSeenOpening = false

// FIX: global sprint state so Timer can access it
var sprintingNow = false

var keyFound = false
var fuseFound = false
var crowbarFound = false
var powerOn = false
var cardFound = false
var noteFound = false
var codeA = false
var codeB = false
var codeSolved = false
var handleFound = false
var finalGateOpen = false
var batteryFound = false
var kitchenDoorOpen = false
var basementDoorOpen = false
var eastDoorOpen = false
var safeRoomOpen = false

val keys =
  Array.fill[Boolean](512)(false)

val zBuffer =
  Array.fill[Double](RAYS)(MAX_DEPTH)

val rainX =
  Array.fill[Double](110)(0.0)

val rainY =
  Array.fill[Double](110)(0.0)

val rainLen =
  Array.fill[Double](110)(0.0)

val dustX =
  Array.fill[Double](55)(0.0)

val dustY =
  Array.fill[Double](55)(0.0)

val mapTemplate = Array(
  "################################################".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........D........##........##........##.....##".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "####.##################################.########".toCharArray,
  "#........D...........D.............D...........#".toCharArray,
  "####.################.##################.#######".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........D........##........##........##.....##".toCharArray,
  "####.################.##################.#######".toCharArray,
  "#...........##......................##........##".toCharArray,
  "#...........##......................##........##".toCharArray,
  "#...........##......................##........##".toCharArray,
  "#...........D.......................D........###".toCharArray,
  "#...........##......................##........##".toCharArray,
  "#...........##......................##........##".toCharArray,
  "####.################.##################.#######".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##....E#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "##........##........##........##........##.....#".toCharArray,
  "################################################".toCharArray
)

val map = mapTemplate.map(_.clone)

val roomNames = Array(
  "West Kitchen",
  "Central Dining",
  "North Library",
  "North Study",
  "East Guest Room",
  "East Gallery",
  "West Bedroom",
  "West Washroom",
  "Grand Hall",
  "Upper Landing",
  "Master Bedroom",
  "Old Nursery",
  "South Workshop",
  "South Storage",
  "Generator Room",
  "Basement Hub",
  "Hidden Safe Room",
  "East Utility",
  "Front Foyer",
  "Rear Passage"
)

val roomX = Array(
  2, 12, 22, 32, 42,
  32, 2, 12, 4, 14,
  24, 34, 2, 12, 22,
  32, 40, 40, 4, 28
)

val roomY = Array(
  1, 1, 1, 1, 1,
  1, 11, 11, 9, 9,
  9, 9, 24, 24, 24,
  24, 18, 18, 27, 18
)

val roomW = Array(
  8, 8, 8, 8, 5,
  8, 8, 8, 24, 8,
  8, 8, 8, 8, 8,
  8, 6, 6, 8, 10
)

val roomH = Array(
  6, 6, 6, 6, 6,
  6, 6, 6, 5, 6,
  6, 6, 6, 6, 6,
  6, 6, 5, 4, 5
)

val patrolX = Array(
  4.5, 16.5, 26.5, 36.5,
  44.0, 36.5, 16.5, 6.5,
  28.5, 42.0, 30.5, 22.0
)

val patrolY = Array(
  4.0, 4.0, 4.0, 4.0,
  4.0, 14.0, 14.0, 14.0,
  11.5, 22.0, 26.0, 26.0
)

val keyX = 5.6
val keyY = 3.8

val fuseX = 25.7
val fuseY = 26.1

val crowbarX = 15.5
val crowbarY = 26.4

val cardX = 44.0
val cardY = 14.2

val noteX = 36.6
val noteY = 3.9

val codeAX = 23.6
val codeAY = 14.3

val codeBX = 43.5
val codeBY = 26.2

val handleX = 41.8
val handleY = 20.7

val batteryX = 6.2
val batteryY = 28.1

val generatorX = 26.5
val generatorY = 27.0

val exitX = 46.1
val exitY = 27.5

val kitchenDoorX = 10.0
val kitchenDoorY = 5.0

val basementDoorX = 31.0
val basementDoorY = 24.0

val eastDoorX = 40.0
val eastDoorY = 20.0

val safeRoomX = 42.0
val safeRoomY = 20.5

val securityX = 45.0
val securityY = 22.0

val hideX = Array(
  7.4,
  43.0,
  18.5,
  5.4,
  34.5
)

val hideY = Array(
  13.3,
  13.0,
  15.0,
  26.0,
  27.2
)

val propX = Array(
  3.0, 6.5, 14.0, 17.0, 24.0, 27.5, 34.5, 37.5, 43.0, 45.0,
  3.0, 6.8, 14.2, 17.4, 24.2, 27.6, 34.3, 37.4, 43.2, 45.0,
  3.0, 6.6, 14.0, 17.2, 24.0, 27.4, 34.4, 37.6, 43.0, 45.0,
  4.0, 7.0, 14.0, 17.0, 24.0, 27.0, 34.0, 37.0, 43.0, 45.0
)

val propY = Array(
  2.8, 5.2, 2.8, 5.2, 2.8, 5.2, 2.8, 5.2, 2.8, 5.2,
  11.8, 14.2, 11.8, 14.2, 11.8, 14.2, 11.8, 14.2, 11.8, 14.2,
  25.0, 28.2, 25.0, 28.2, 25.0, 28.2, 25.0, 28.2, 25.0, 28.2,
  19.0, 22.0, 19.0, 22.0, 19.0, 22.0, 19.0, 22.0, 19.0, 22.0
)

val propKind = Array(
  "TABLE", "SHELF", "BED", "DESK", "TABLE", "OVEN", "PICTURE", "PIPE", "BED", "SHELF",
  "BED", "PICTURE", "SHELF", "BED", "PIPE", "DESK", "BED", "PICTURE", "BED", "BARREL",
  "CRATE", "CRATE", "GENERATOR", "BARREL", "CRATE", "CRATE", "PIPE", "GENERATOR", "SHELF", "BARREL",
  "DESK", "SHELF", "CRATE", "PIPE", "SINK", "TABLE", "PICTURE", "SHELF", "BED", "CRATE"
)

val lampX = Array(
  5.0, 15.0, 25.0, 35.0, 44.0,
  7.0, 17.0, 27.0, 37.0, 45.0,
  6.0, 16.0, 26.0, 36.0, 44.0
)

val lampY = Array(
  2.2, 2.2, 2.2, 2.2, 2.2,
  12.2, 12.2, 12.2, 12.2, 12.2,
  25.2, 25.2, 25.2, 25.2, 25.2
)

val lampKind = Array(
  "LAMP", "LAMP", "LAMP", "LAMP", "LAMP",
  "LAMP", "LAMP", "LAMP", "LAMP", "LAMP",
  "LAMP", "LAMP", "LAMP", "LAMP", "LAMP"
)

val wallMarks = Array(
  Array(6.5, 1.02, 0),
  Array(16.5, 1.02, 0),
  Array(26.5, 1.02, 0),
  Array(36.5, 1.02, 0),
  Array(44.5, 1.02, 0),

  Array(2.02, 4.8, 1),
  Array(12.02, 4.8, 1),
  Array(22.02, 4.8, 1),
  Array(32.02, 4.8, 1),
  Array(42.02, 4.8, 1),

  Array(6.5, 30.98, 0),
  Array(16.5, 30.98, 0),
  Array(26.5, 30.98, 0),
  Array(36.5, 30.98, 0),
  Array(44.5, 30.98, 0)
)

var objectiveIndex = 0

// ---------------------------------------------------------------------------
// BASIC HELPERS
// ---------------------------------------------------------------------------

def clamp(
  v: Double,
  lo: Double,
  hi: Double
): Double = {
  Math.max(lo, Math.min(hi, v))
}

def dist(
  ax: Double,
  ay: Double,
  bx: Double,
  by: Double
): Double = {

  val dx = ax - bx
  val dy = ay - by

  Math.sqrt(
    dx * dx + dy * dy
  )
}

def angleNorm(
  v: Double
): Double = {

  var a = v

  while (a > Math.PI) {
    a -= PI2
  }

  while (a < -Math.PI) {
    a += PI2
  }

  a
}

def fmt1(
  v: Double
): String = {

  (
    Math.round(
      v * 10.0
    ).toDouble / 10.0
  ).toString
}

def fmtTime(
  t: Double
): String = {

  val total =
    Math.max(
      0,
      t.toInt
    )

  val m =
    total / 60

  val s =
    total % 60

  (
    if (m < 10) "0" else ""
  ) +
  m +
  ":" +
  (
    if (s < 10) "0" else ""
  ) +
  s
}

def setMessage(
  s: String,
  ticks: Int
): Unit = {

  message = s
  messageTicks = ticks
}

def roomNameAt(
  x: Double,
  y: Double
): String = {

  var i = 0

  while (i < roomNames.length) {

    if (
      x >= roomX(i) &&
      x <= roomX(i) + roomW(i) &&
      y >= roomY(i) &&
      y <= roomY(i) + roomH(i)
    ) {
      return roomNames(i)
    }

    i += 1
  }

  if (
    y >= 8.2 &&
    y <= 9.4
  ) {
    "North Corridor"
  }
  else if (
    y >= 16.2 &&
    y <= 17.3
  ) {
    "Central Corridor"
  }
  else if (
    y >= 23.3 &&
    y <= 24.5
  ) {
    "South Corridor"
  }
  else {
    "Connecting Passage"
  }
}

def currentObjective(): String = {

  if (!keyFound) {
    "Find the brass house key in the West Kitchen."
  }
  else if (!kitchenDoorOpen) {
    "Use the brass key at the kitchen service door."
  }
  else if (!crowbarFound) {
    "Find the old crowbar in South Storage."
  }
  else if (!fuseFound) {
    "Find the emergency fuse in the Generator Room."
  }
  else if (!powerOn) {
    "Install the fuse and restore the generator."
  }
  else if (!cardFound) {
    "Find the blue access card in the East Gallery."
  }
  else if (!noteFound) {
    "Find the security note in the North Study."
  }
  else if (!codeA || !codeB) {
    "Find both security-code fragments."
  }
  else if (!codeSolved) {
    "Use the security terminal and enter the 4-digit code."
  }
  else if (!handleFound) {
    "Find the old gate handle in the Hidden Safe Room."
  }
  else {
    "Return to the front exit and escape the house."
  }
}

def allObjectives(): Boolean = {

  keyFound &&
  kitchenDoorOpen &&
  crowbarFound &&
  fuseFound &&
  powerOn &&
  cardFound &&
  noteFound &&
  codeA &&
  codeB &&
  codeSolved &&
  handleFound
}

// ---------------------------------------------------------------------------
// WORLD / COLLISION
// ---------------------------------------------------------------------------

def cellAt(
  x: Int,
  y: Int
): Char = {

  if (
    x < 0 ||
    y < 0 ||
    x >= MAP_W ||
    y >= MAP_H
  ) {
    '#'
  }
  else {
    map(y)(x)
  }
}

def doorAt(
  x: Int,
  y: Int
): Boolean = {

  (
    x == 10 && y == 5
  ) ||
  (
    x == 10 && y == 15
  ) ||
  (
    x == 10 && y == 21
  ) ||
  (
    x == 20 && y == 9
  ) ||
  (
    x == 20 && y == 16
  ) ||
  (
    x == 20 && y == 23
  ) ||
  (
    x == 30 && y == 9
  ) ||
  (
    x == 30 && y == 16
  ) ||
  (
    x == 30 && y == 23
  ) ||
  (
    x == 40 && y == 9
  ) ||
  (
    x == 40 && y == 16
  ) ||
  (
    x == 40 && y == 20
  ) ||
  (
    x == 10 && y == 24
  ) ||
  (
    x == 20 && y == 24
  ) ||
  (
    x == 30 && y == 24
  ) ||
  (
    x == 40 && y == 24
  )
}

def doorOpenAt(
  x: Int,
  y: Int
): Boolean = {

  if (
    (x == 10 && y == 5) ||
    (x == 10 && y == 15) ||
    (x == 10 && y == 21)
  ) {
    kitchenDoorOpen
  }
  else if (
    x == 40 &&
    y == 20
  ) {
    eastDoorOpen
  }
  else if (
    x == 30 &&
    y == 24
  ) {
    basementDoorOpen
  }
  else {
    safeRoomOpen ||
    kitchenDoorOpen
  }
}

def blockedCell(
  x: Int,
  y: Int
): Boolean = {

  val c =
    cellAt(x, y)

  if (c == '#') {
    true
  }
  else if (
    c == 'E' &&
    !finalGateOpen
  ) {
    true
  }
  else if (
    doorAt(x, y) &&
    !doorOpenAt(x, y)
  ) {
    true
  }
  else {
    false
  }
}

def blocked(
  x: Double,
  y: Double
): Boolean = {

  val r =
    PLAYER_RADIUS

  blockedCell(
    Math.floor(x - r).toInt,
    Math.floor(y - r).toInt
  ) ||
  blockedCell(
    Math.floor(x + r).toInt,
    Math.floor(y - r).toInt
  ) ||
  blockedCell(
    Math.floor(x - r).toInt,
    Math.floor(y + r).toInt
  ) ||
  blockedCell(
    Math.floor(x + r).toInt,
    Math.floor(y + r).toInt
  )
}

def moveEntity(
  x: Double,
  y: Double,
  nx: Double,
  ny: Double
): (Double, Double) = {

  var xx = nx
  var yy = ny

  if (blocked(xx, y)) {
    xx = x
  }

  if (blocked(xx, yy)) {
    yy = y
  }

  (xx, yy)
}

def lineOfSight(
  ax: Double,
  ay: Double,
  bx: Double,
  by: Double
): Boolean = {

  val d =
    dist(
      ax,
      ay,
      bx,
      by
    )

  val steps =
    Math.max(
      1,
      (d / 0.06).toInt
    )

  var i = 1

  while (i < steps) {

    val t =
      i.toDouble /
      steps.toDouble

    val x =
      ax +
      (bx - ax) * t

    val y =
      ay +
      (by - ay) * t

    if (
      blockedCell(
        Math.floor(x).toInt,
        Math.floor(y).toInt
      )
    ) {
      return false
    }

    i += 1
  }

  true
}

// ---------------------------------------------------------------------------
// SAVE
// ---------------------------------------------------------------------------

def loadBestTime(): Double = {

  try {

    val p =
      Paths.get(
        "old_house_night_watch_best_time.txt"
      )

    if (Files.exists(p)) {

      new String(
        Files.readAllBytes(p),
        "UTF-8"
      ).trim.toDouble
    }
    else {
      0.0
    }
  }
  catch {
    case _: Exception => 0.0
  }
}

def saveBestTime(
  v: Double
): Unit = {

  try {

    Files.write(
      Paths.get(
        "old_house_night_watch_best_time.txt"
      ),
      v.toString.getBytes("UTF-8")
    )
  }
  catch {
    case _: Exception =>
  }
}

bestTime =
  loadBestTime()

def safeTone(): Unit = {

  if (
    muted ||
    animTick -
      lastSoundTick < 7L
  ) {
    return
  }

  lastSoundTick =
    animTick

  try {
    Toolkit
      .getDefaultToolkit
      .beep()
  }
  catch {
    case _: Exception =>
  }
}

// ---------------------------------------------------------------------------
// RESET / GAME START
// ---------------------------------------------------------------------------

def resetWorld(): Unit = {

  var y = 0

  while (y < MAP_H) {

    var x = 0

    while (x < MAP_W) {

      map(y)(x) =
        mapTemplate(y)(x)

      x += 1
    }

    y += 1
  }

  px = 4.5
  py = 27.5
  pa = -Math.PI / 2.0

  ex = 30.5
  ey = 4.5
  ea = Math.PI

  enemyState = "PATROL"

  enemyTargetX =
    patrolX(0)

  enemyTargetY =
    patrolY(0)

  enemyDistance = 99.0
  enemyVisible = false
  enemyStun = 0.0
  enemySearch = 0.0
  enemyWander = 0

  keyFound = false
  fuseFound = false
  crowbarFound = false
  powerOn = false
  cardFound = false
  noteFound = false

  codeA = false
  codeB = false
  codeSolved = false
  handleFound = false
  finalGateOpen = false

  batteryFound = false
  kitchenDoorOpen = false
  basementDoorOpen = false
  eastDoorOpen = false
  safeRoomOpen = false

  hidden = false

  gameTime = 0.0
  score = 0
  health = 100.0
  stamina = 100.0
  battery = 100.0
  fear = 0.0
  noiseLevel = 0.0

  message =
    "You wake in the rear foyer. Find a way through the old house."

  messageTicks = 360

  paused = false
  running = true

  scareFlash = 0
  lightning = 0
  doorPulse = 0
  exitGlow = 0

  objectiveIndex = 0

  crouched = false
  sprintingNow = false

  headBob = 0.0
  cameraBobX = 0.0
  cameraBobY = 0.0
  footstepPulse = 0.0
  screenShake = 0.0
  interactionPulse = 0.0

  totalDiscoveries = 0

  var di = 0

  while (
    di < discoveredRooms.length
  ) {

    discoveredRooms(di) = false

    di += 1
  }

  lastNoiseX = px
  lastNoiseY = py
  lastPlayerX = px
  lastPlayerY = py
}

def beginGame(): Unit = {

  resetWorld()

  state = "PLAYING"

  safeTone()
}

def setDifficulty(
  name: String
): Unit = {

  difficulty = name

  if (name == "EASY") {

    difficultyScale = 0.80
    enemySpeedScale = 0.82
    detectionScale = 0.74
    batteryDrain = 0.017
  }
  else if (name == "HARD") {

    difficultyScale = 1.20
    enemySpeedScale = 1.20
    detectionScale = 1.28
    batteryDrain = 0.030
  }
  else {

    difficultyScale = 1.0
    enemySpeedScale = 1.0
    detectionScale = 1.0
    batteryDrain = 0.023
  }

  setMessage(
    "Difficulty: " + name,
    140
  )
}

// ---------------------------------------------------------------------------
// PICKUPS / PUZZLES
// ---------------------------------------------------------------------------

def collectNearby(): Unit = {

  if (
    !keyFound &&
    dist(
      px,
      py,
      keyX,
      keyY
    ) < 0.72
  ) {

    keyFound = true
    score += 100

    safeTone()

    setMessage(
      "BRASS KEY FOUND. A service door can now be opened.",
      220
    )
  }

  if (
    !crowbarFound &&
    kitchenDoorOpen &&
    dist(
      px,
      py,
      crowbarX,
      crowbarY
    ) < 0.72
  ) {

    crowbarFound = true
    score += 110

    safeTone()

    setMessage(
      "CROWBAR FOUND. Metal panels can be opened.",
      200
    )
  }

  if (
    !fuseFound &&
    crowbarFound &&
    dist(
      px,
      py,
      fuseX,
      fuseY
    ) < 0.72
  ) {

    fuseFound = true
    score += 120

    safeTone()

    setMessage(
      "EMERGENCY FUSE FOUND.",
      170
    )
  }

  if (
    !cardFound &&
    powerOn &&
    dist(
      px,
      py,
      cardX,
      cardY
    ) < 0.72
  ) {

    cardFound = true
    score += 150

    safeTone()

    setMessage(
      "BLUE ACCESS CARD FOUND.",
      170
    )
  }

  if (
    !noteFound &&
    powerOn &&
    dist(
      px,
      py,
      noteX,
      noteY
    ) < 0.72
  ) {

    noteFound = true
    score += 85

    safeTone()

    setMessage(
      "SECURITY NOTE FOUND. Two fragments remain.",
      200
    )
  }

  if (
    !codeA &&
    powerOn &&
    dist(
      px,
      py,
      codeAX,
      codeAY
    ) < 0.72
  ) {

    codeA = true
    score += 70

    safeTone()

    setMessage(
      "CODE FRAGMENT A FOUND.",
      140
    )
  }

  if (
    !codeB &&
    powerOn &&
    dist(
      px,
      py,
      codeBX,
      codeBY
    ) < 0.72
  ) {

    codeB = true
    score += 70

    safeTone()

    setMessage(
      "CODE FRAGMENT B FOUND.",
      140
    )
  }

  if (
    !handleFound &&
    codeSolved &&
    cardFound &&
    dist(
      px,
      py,
      handleX,
      handleY
    ) < 0.72
  ) {

    handleFound = true
    score += 200

    safeTone()

    setMessage(
      "EXIT HANDLE FOUND. Return to the front gate.",
      230
    )
  }

  if (
    !batteryFound &&
    dist(
      px,
      py,
      batteryX,
      batteryY
    ) < 0.72
  ) {

    batteryFound = true
    battery = 100.0
    score += 30

    safeTone()

    setMessage(
      "Flashlight battery replaced.",
      140
    )
  }
}

def useKitchenDoor(): Unit = {

  if (kitchenDoorOpen) {
    return
  }

  if (
    dist(
      px,
      py,
      kitchenDoorX,
      kitchenDoorY
    ) < 1.25
  ) {

    if (keyFound) {

      kitchenDoorOpen = true
      doorPulse = 24
      score += 90

      safeTone()

      setMessage(
        "SERVICE DOOR UNLOCKED.",
        160
      )
    }
    else {

      setMessage(
        "Locked. Find the brass key first.",
        140
      )

      safeTone()
    }
  }
}

def useGenerator(): Unit = {

  if (powerOn) {
    return
  }

  if (
    dist(
      px,
      py,
      generatorX,
      generatorY
    ) < 1.25
  ) {

    if (
      fuseFound &&
      crowbarFound
    ) {

      powerOn = true
      score += 220
      doorPulse = 30

      safeTone()

      setMessage(
        "GENERATOR ONLINE. Security systems are awake.",
        260
      )
    }
    else {

      setMessage(
        "The generator needs an emergency fuse and a tool.",
        170
      )
    }
  }
}

def useBasementDoor(): Unit = {

  if (basementDoorOpen) {
    return
  }

  if (
    dist(
      px,
      py,
      basementDoorX,
      basementDoorY
    ) < 1.30
  ) {

    if (codeSolved) {

      basementDoorOpen = true
      score += 120

      safeTone()

      setMessage(
        "BASEMENT DOOR UNLOCKED.",
        160
      )
    }
    else {

      setMessage(
        "A security lock blocks this door.",
        140
      )
    }
  }
}

def useEastDoor(): Unit = {

  if (eastDoorOpen) {
    return
  }

  if (
    dist(
      px,
      py,
      eastDoorX,
      eastDoorY
    ) < 1.30
  ) {

    if (cardFound) {

      eastDoorOpen = true

      safeTone()

      setMessage(
        "ACCESS CARD ACCEPTED. EAST UTILITY OPEN.",
        180
      )
    }
    else {

      setMessage(
        "Card reader: access denied.",
        140
      )
    }
  }
}

def useSafeRoom(): Unit = {

  if (safeRoomOpen) {
    return
  }

  if (
    dist(
      px,
      py,
      safeRoomX,
      safeRoomY
    ) < 1.35
  ) {

    if (
      codeSolved &&
      cardFound
    ) {

      safeRoomOpen = true
      score += 130

      safeTone()

      setMessage(
        "HIDDEN SAFE ROOM UNSEALED.",
        190
      )
    }
    else {

      setMessage(
        "Two locks are still active here.",
        140
      )
    }
  }
}

def solveSecurity(): Unit = {

  if (codeSolved) {
    return
  }

  if (
    !codeA ||
    !codeB ||
    !noteFound ||
    !cardFound
  ) {
    return
  }

  if (
    dist(
      px,
      py,
      securityX,
      securityY
    ) < 1.35 &&
    powerOn
  ) {

    val entry =
      JOptionPane.showInputDialog(
        frame,
        "SECURITY PANEL\nEnter the 4-digit house code from the note and fragments:",
        "Security Code",
        JOptionPane.QUESTION_MESSAGE
      )

    if (entry != null) {

      if (entry.trim == "5814") {

        codeSolved = true
        score += 260

        safeTone()

        setMessage(
          "SECURITY CODE ACCEPTED. Restricted doors released.",
          260
        )
      }
      else {

        fear =
          clamp(
            fear + 10.0,
            0.0,
            100.0
          )

        score =
          Math.max(
            0,
            score - 20
          )

        noiseLevel = 1.0

        lastNoiseX = px
        lastNoiseY = py

        safeTone()

        setMessage(
          "Wrong code. An alarm pulse echoed through the house.",
          180
        )
      }
    }
  }
}

def attemptEscape(): Unit = {

  if (
    dist(
      px,
      py,
      exitX,
      exitY
    ) < 1.35
  ) {

    if (allObjectives()) {

      finalGateOpen = true
      state = "WIN"
      running = false

      val bonus =
        Math.max(
          0,
          (
            2400.0 -
            gameTime * 18.0
          ).toInt
        )

      score += bonus

      if (
        bestTime == 0.0 ||
        gameTime < bestTime
      ) {

        bestTime =
          gameTime

        saveBestTime(
          bestTime
        )
      }

      safeTone()

      setMessage(
        "YOU ESCAPED THE NIGHT HOUSE.",
        300
      )
    }
    else {

      setMessage(
        "Gate locked. " +
        currentObjective(),
        190
      )
    }
  }
}

def interact(): Unit = {

  collectNearby()
  useKitchenDoor()
  useGenerator()
  useBasementDoor()
  useEastDoor()
  useSafeRoom()
  solveSecurity()
  attemptEscape()
}

// ---------------------------------------------------------------------------
// PLAYER / NOISE
// ---------------------------------------------------------------------------

def toggleHide(): Unit = {

  var near = false
  var i = 0

  while (i < hideX.length) {

    if (
      dist(
        px,
        py,
        hideX(i),
        hideY(i)
      ) < 1.1
    ) {
      near = true
    }

    i += 1
  }

  if (near) {

    hidden = !hidden

    if (hidden) {

      setMessage(
        "HIDDEN. Stay quiet.",
        150
      )
    }
    else {

      setMessage(
        "You leave hiding.",
        100
      )
    }
  }
  else {

    setMessage(
      "No hiding place nearby.",
      110
    )
  }
}

def updatePlayer(
  dt: Double
): Unit = {

  val forward =
    if (
      keys(KeyEvent.VK_W) ||
      keys(KeyEvent.VK_UP)
    ) {
      1.0
    }
    else {
      0.0
    }

  val back =
    if (
      keys(KeyEvent.VK_S) ||
      keys(KeyEvent.VK_DOWN)
    ) {
      1.0
    }
    else {
      0.0
    }

  val left =
    if (
      keys(KeyEvent.VK_A) ||
      keys(KeyEvent.VK_LEFT)
    ) {
      1.0
    }
    else {
      0.0
    }

  val right =
    if (
      keys(KeyEvent.VK_D) ||
      keys(KeyEvent.VK_RIGHT)
    ) {
      1.0
    }
    else {
      0.0
    }

  if (
    keys(KeyEvent.VK_C) ||
    keys(KeyEvent.VK_V)
  ) {
    crouched = true
  }
  else {
    crouched = false
  }

  val sprinting =
    (
      keys(KeyEvent.VK_SHIFT) ||
      keys(KeyEvent.VK_CONTROL)
    ) &&
    stamina > 3.0 &&
    (
      forward + back
    ) != 0.0 &&
    !hidden &&
    !crouched

  // FIX: make current sprint state available outside this method
  sprintingNow = sprinting

  var speed =
    if (sprinting) {
      SPRINT_SPEED
    }
    else {
      WALK_SPEED
    }

  if (crouched) {
    speed *= 0.48
  }

  if (hidden) {
    speed *= 0.25
  }

  var dx = 0.0
  var dy = 0.0

  dx +=
    Math.cos(pa) *
    (
      forward - back
    ) *
    speed

  dy +=
    Math.sin(pa) *
    (
      forward - back
    ) *
    speed

  dx +=
    Math.cos(
      pa + Math.PI / 2.0
    ) *
    (
      right - left
    ) *
    STRAFE_SPEED

  dy +=
    Math.sin(
      pa + Math.PI / 2.0
    ) *
    (
      right - left
    ) *
    STRAFE_SPEED

  val moved =
    Math.sqrt(
      dx * dx +
      dy * dy
    )

  val newPos =
    moveEntity(
      px,
      py,
      px + dx,
      py + dy
    )

  px =
    newPos._1

  py =
    newPos._2

  if (
    sprinting &&
    moved > 0.001
  ) {

    stamina =
      clamp(
        stamina - 0.62,
        0.0,
        100.0
      )

    noiseLevel =
      clamp(
        noiseLevel + 0.045,
        0.0,
        1.0
      )
  }
  else if (
    crouched &&
    moved > 0.001
  ) {

    stamina =
      clamp(
        stamina + 0.22,
        0.0,
        100.0
      )

    noiseLevel =
      clamp(
        noiseLevel - 0.010,
        0.0,
        1.0
      )
  }
  else {

    stamina =
      clamp(
        stamina + 0.38,
        0.0,
        100.0
      )

    noiseLevel =
      clamp(
        noiseLevel - 0.025,
        0.0,
        1.0
      )
  }

  if (
    moved > 0.001
  ) {

    lastNoiseX = px
    lastNoiseY = py

    lastPlayerX = px
    lastPlayerY = py

    // FIX:
    // Scala/Kojo-safe if expression
    headBob += (
      if (sprinting)
        0.24
      else
        0.12
    )

    footstepPulse =
      clamp(
        footstepPulse +
        (
          if (sprinting)
            0.18
          else
            0.10
        ),
        0.0,
        1.0
      )
  }
  else {

    footstepPulse =
      clamp(
        footstepPulse - 0.08,
        0.0,
        1.0
      )
  }

  cameraBobX =
    Math.sin(headBob) *
    (
      if (crouched)
        1.0
      else
        2.8
    )

  cameraBobY =
    Math.abs(
      Math.cos(headBob)
    ) *
    (
      if (sprinting)
        5.0
      else
        2.2
    )

  if (
    flashlightOn &&
    battery > 0.0
  ) {

    battery =
      clamp(
        battery - batteryDrain,
        0.0,
        100.0
      )
  }

  if (
    battery <= 0.0
  ) {
    flashlightOn = false
  }

  if (fear > 0.0) {

    fear =
      clamp(
        fear - 0.02,
        0.0,
        100.0
      )
  }

  if (
    enemyDistance < 5.0 &&
    enemyState == "CHASE" &&
    !hidden
  ) {

    fear =
      clamp(
        fear + 0.20,
        0.0,
        100.0
      )
  }

  if (
    enemyState == "CHASE" &&
    enemyDistance < 3.0
  ) {

    screenShake =
      clamp(
        screenShake + 0.9,
        0.0,
        8.0
      )
  }
  else {

    screenShake =
      clamp(
        screenShake - 0.4,
        0.0,
        8.0
      )
  }

  if (
    messageTicks <= 0
  ) {

    val rn =
      roomNameAt(
        px,
        py
      )

    var rIndex = 0

    while (
      rIndex < roomNames.length
    ) {

      if (
        rn == roomNames(rIndex) &&
        !discoveredRooms(rIndex)
      ) {

        discoveredRooms(rIndex) =
          true

        totalDiscoveries += 1

        if (
          totalDiscoveries <=
          roomNames.length
        ) {
          score += 5
        }

        rIndex =
          roomNames.length
      }
      else {
        rIndex += 1
      }
    }
  }

  if (
    health <= 0.0
  ) {

    state = "LOSE"
    running = false
  }
}

// ---------------------------------------------------------------------------
// ENEMY AI
// ---------------------------------------------------------------------------

def setEnemyTargetToPatrol(): Unit = {

  if (
    patrolX.length == 0
  ) {
    return
  }

  val idx =
    enemyWander %
    patrolX.length

  enemyTargetX =
    patrolX(idx)

  enemyTargetY =
    patrolY(idx)
}

def updateEnemy(
  dt: Double
): Unit = {

  enemyDistance =
    dist(
      px,
      py,
      ex,
      ey
    )

  enemyVisible =
    lineOfSight(
      ex,
      ey,
      px,
      py
    ) &&
    Math.abs(
      angleNorm(
        Math.atan2(
          py - ey,
          px - ex
        ) - ea
      )
    ) <
    Math.PI *
    (
      if (crouched)
        0.58
      else
        0.78
    )

  if (
    enemyStun > 0.0
  ) {

    enemyStun =
      Math.max(
        0.0,
        enemyStun - dt
      )
  }

  if (hidden) {

    if (
      enemyDistance < 4.0 &&
      enemyState == "CHASE"
    ) {

      enemyState = "SEARCH"
      enemySearch = 4.5
      enemyTargetX = lastNoiseX
      enemyTargetY = lastNoiseY
    }
  }
  else {

    if (
      enemyVisible &&
      enemyDistance <
        (
          if (crouched)
            6.4
          else
            9.5
        ) *
        detectionScale
    ) {

      enemyState = "CHASE"
      enemyTargetX = px
      enemyTargetY = py
      enemySearch = 0.0
    }
    else if (
      noiseLevel > 0.30 &&
      enemyDistance <
        (
          if (crouched)
            8.0
          else
            12.0
        ) *
        detectionScale
    ) {

      enemyState = "SEARCH"
      enemySearch = 5.5
      enemyTargetX = lastNoiseX
      enemyTargetY = lastNoiseY
    }
  }

  if (
    enemyState == "SEARCH"
  ) {

    enemySearch -= dt

    if (
      enemySearch <= 0.0
    ) {

      enemyState = "PATROL"
      enemyWander += 1

      setEnemyTargetToPatrol()
    }
    else if (
      dist(
        ex,
        ey,
        enemyTargetX,
        enemyTargetY
      ) < 0.7
    ) {

      enemyWander += 1

      enemyTargetX =
        ex +
        Math.cos(
          animTick.toDouble *
          0.03
        ) *
        2.2

      enemyTargetY =
        ey +
        Math.sin(
          animTick.toDouble *
          0.027
        ) *
        2.2
    }
  }

  if (
    enemyState == "PATROL"
  ) {

    if (
      dist(
        ex,
        ey,
        enemyTargetX,
        enemyTargetY
      ) < 0.75
    ) {

      enemyWander += 1

      setEnemyTargetToPatrol()
    }
  }

  if (
    enemyState == "CHASE"
  ) {

    enemyTargetX = px
    enemyTargetY = py
  }

  val targetAng =
    Math.atan2(
      enemyTargetY - ey,
      enemyTargetX - ex
    )

  ea =
    angleNorm(
      ea +
      clamp(
        angleNorm(
          targetAng - ea
        ),
        -ROT_SPEED,
        ROT_SPEED
      )
    )

  val chaseSpeed =
    if (
      enemyState == "CHASE"
    ) {
      0.075 *
      enemySpeedScale
    }
    else if (
      enemyState == "SEARCH"
    ) {
      0.054 *
      enemySpeedScale
    }
    else {
      0.037 *
      enemySpeedScale
    }

  if (
    enemyStun <= 0.0
  ) {

    val nx =
      ex +
      Math.cos(ea) *
      chaseSpeed

    val ny =
      ey +
      Math.sin(ea) *
      chaseSpeed

    val p =
      moveEntity(
        ex,
        ey,
        nx,
        ny
      )

    ex =
      p._1

    ey =
      p._2
  }

  if (
    !hidden &&
    enemyDistance < 0.72 &&
    enemyStun <= 0.0
  ) {

    health =
      clamp(
        health -
        0.95 *
        difficultyScale,
        0.0,
        100.0
      )

    fear = 100.0
    scareFlash = 18
    enemyState = "CHASE"
  }

  if (
    flashlightOn &&
    enemyVisible &&
    enemyDistance < 6.0 &&
    enemyStun <= 0.0 &&
    battery > 2.0
  ) {

    val headAng =
      Math.atan2(
        ey - py,
        ex - px
      )

    val diff =
      Math.abs(
        angleNorm(
          headAng - pa
        )
      )

    if (
      diff < 0.18
    ) {

      enemyStun = 2.5
      enemyState = "STUNNED"
      score += 15

      safeTone()

      setMessage(
        "The flashlight stunned the Watcher briefly.",
        100
      )
    }
  }

  if (
    enemyState == "STUNNED" &&
    enemyStun <= 0.0
  ) {

    enemyState = "PATROL"
    enemyWander += 1

    setEnemyTargetToPatrol()
  }
}

// ---------------------------------------------------------------------------
// 3D HELPERS
// ---------------------------------------------------------------------------

def safeDepth(
  d: Double
): Double = {

  val v =
    if (d < 0.05)
      0.05
    else
      d

  if (
    v.isNaN ||
    v.isInfinite
  ) {
    MAX_DEPTH
  }
  else {
    v
  }
}

def projectX(
  angleOffset: Double
): Double = {
  angleOffset
}

def wallShade(
  distValue: Double,
  hitSide: Int,
  wallX: Int,
  wallY: Int
): Color = {

  val base =
    (
      150.0 -
      distValue * 3.0
    ).toInt

  val variation =
    (
      (
        wallX * 17 +
        wallY * 11
      ) %
      24
    ) -
    12

  val sideBoost =
    if (hitSide == 1)
      -18
    else
      0

  val r =
    clamp(
      (
        base +
        variation +
        sideBoost
      ).toDouble,
      28.0,
      150.0
    ).toInt

  val gg =
    clamp(
      (
        base +
        variation +
        sideBoost +
        5
      ).toDouble,
      30.0,
      165.0
    ).toInt

  val b =
    clamp(
      (
        base +
        variation +
        sideBoost +
        10
      ).toDouble,
      38.0,
      180.0
    ).toInt

  new Color(
    r,
    gg,
    b
  )
}

def wallTextureOffset(
  hitX: Double,
  hitY: Double,
  side: Int
): Double = {

  val frac =
    if (side == 0)
      hitY -
      Math.floor(hitY)
    else
      hitX -
      Math.floor(hitX)

  frac - 0.5
}

def castRay(
  rayAngle: Double,
  index: Int
): Double = {

  val dirX =
    Math.cos(rayAngle)

  val dirY =
    Math.sin(rayAngle)

  var mapX =
    Math.floor(px).toInt

  var mapY =
    Math.floor(py).toInt

  val deltaDistX =
    if (
      Math.abs(dirX) <
      0.00001
    ) {
      1.0e30
    }
    else {
      Math.abs(
        1.0 / dirX
      )
    }

  val deltaDistY =
    if (
      Math.abs(dirY) <
      0.00001
    ) {
      1.0e30
    }
    else {
      Math.abs(
        1.0 / dirY
      )
    }

  var stepX = 1
  var stepY = 1

  var sideDistX = 0.0
  var sideDistY = 0.0

  if (dirX < 0.0) {

    stepX = -1

    sideDistX =
      (
        px -
        mapX
      ) *
      deltaDistX
  }
  else {

    sideDistX =
      (
        mapX +
        1.0 -
        px
      ) *
      deltaDistX
  }

  if (dirY < 0.0) {

    stepY = -1

    sideDistY =
      (
        py -
        mapY
      ) *
      deltaDistY
  }
  else {

    sideDistY =
      (
        mapY +
        1.0 -
        py
      ) *
      deltaDistY
  }

  var hit = false
  var side = 0
  var depth = 0.0
  var loop = 0

  while (
    !hit &&
    depth < MAX_DEPTH &&
    loop < 120
  ) {

    if (
      sideDistX <
      sideDistY
    ) {

      sideDistX +=
        deltaDistX

      mapX += stepX
      side = 0
    }
    else {

      sideDistY +=
        deltaDistY

      mapY += stepY
      side = 1
    }

    depth =
      if (side == 0)
        sideDistX -
        deltaDistX
      else
        sideDistY -
        deltaDistY

    val c =
      cellAt(
        mapX,
        mapY
      )

    if (
      c == '#' ||
      c == 'E' ||
      (
        doorAt(
          mapX,
          mapY
        ) &&
        !doorOpenAt(
          mapX,
          mapY
        )
      )
    ) {
      hit = true
    }

    loop += 1
  }

  val safe =
    safeDepth(depth)

  zBuffer(index) =
    safe

  safe
}

// ---------------------------------------------------------------------------
// RENDERING
// ---------------------------------------------------------------------------

def drawSkyFloor(
  g: Graphics2D
): Unit = {

  val h =
    VIEW_H

  var y = 0

  while (
    y < h / 2
  ) {

    val t =
      y.toDouble /
      (
        h / 2
      ).toDouble

    val r =
      (
        8 +
        20 * t
      ).toInt

    val gg =
      (
        12 +
        18 * t
      ).toInt

    val b =
      (
        20 +
        22 * t
      ).toInt

    g.setColor(
      new Color(
        r,
        gg,
        b
      )
    )

    g.drawLine(
      0,
      y,
      GAME_W,
      y
    )

    y += 1
  }

  y =
    h / 2

  while (
    y < h
  ) {

    val t =
      (
        y -
        h / 2
      ).toDouble /
      (
        h / 2
      ).toDouble

    val c =
      (
        14 +
        24 * t
      ).toInt

    g.setColor(
      new Color(
        c,
        c,
        c + 7
      )
    )

    g.drawLine(
      0,
      y,
      GAME_W,
      y
    )

    y += 1
  }
}

def drawWallView(
  g: Graphics2D
): Unit = {

  drawSkyFloor(g)
  drawCeilingDetails(g)
  drawPerspectiveFloor(g)

  var r = 0

  while (
    r < RAYS
  ) {

    val camera =
      r.toDouble /
      RAYS.toDouble

    val rayAngle =
      pa -
      HALF_FOV +
      camera *
      FOV

    val corrected =
      castRay(
        rayAngle,
        r
      )

    val correctedDepth =
      safeDepth(
        corrected *
        Math.cos(
          rayAngle - pa
        )
      )

    val lineH =
      clamp(
        VIEW_H.toDouble /
        correctedDepth,
        1.0,
        VIEW_H.toDouble *
        1.6
      ).toInt

    val top =
      (
        VIEW_H / 2 +
        cameraBobY.toInt
      ) -
      lineH / 2

    val sx =
      (
        r.toDouble *
        GAME_W.toDouble /
        RAYS.toDouble +
        cameraBobX
      ).toInt

    val exx =
      (
        (
          r + 1
        ).toDouble *
        GAME_W.toDouble /
        RAYS.toDouble +
        cameraBobX
      ).toInt +
      1

    val midX =
      px +
      Math.cos(
        rayAngle
      ) *
      corrected

    val midY =
      py +
      Math.sin(
        rayAngle
      ) *
      corrected

    val cellX =
      Math.floor(
        midX
      ).toInt

    val cellY =
      Math.floor(
        midY
      ).toInt

    val hitSide =
      if (
        Math.abs(
          Math.sin(rayAngle)
        ) >
        Math.abs(
          Math.cos(rayAngle)
        )
      ) {
        1
      }
      else {
        0
      }

    var c =
      wallShade(
        correctedDepth,
        hitSide,
        cellX,
        cellY
      )

    if (
      flashlightOn &&
      battery > 0.0
    ) {

      val centerDiff =
        Math.abs(
          angleNorm(
            rayAngle - pa
          )
        )

      if (
        centerDiff < 0.27
      ) {

        val boost =
          clamp(
            (
              1.0 -
              correctedDepth /
              9.0
            ) *
            70.0,
            0.0,
            70.0
          ).toInt

        c =
          new Color(
            Math.min(
              220,
              c.getRed + boost
            ),
            Math.min(
              225,
              c.getGreen + boost
            ),
            Math.min(
              235,
              c.getBlue + boost
            )
          )
      }
    }

    if (
      lightning > 0
    ) {

      c =
        new Color(
          Math.min(
            255,
            c.getRed + 80
          ),
          Math.min(
            255,
            c.getGreen + 80
          ),
          Math.min(
            255,
            c.getBlue + 85
          )
        )
    }

    g.setColor(c)

    g.fillRect(
      sx,
      top,
      exx - sx,
      lineH
    )

    if (
      r % 9 == 0
    ) {

      val tx =
        wallTextureOffset(
          midX,
          midY,
          hitSide
        )

      val line =
        clamp(
          (
            tx + 0.5
          ) *
          90.0,
          0.0,
          89.0
        ).toInt

      g.setColor(
        new Color(
          Math.min(
            255,
            c.getRed +
            line / 8
          ),
          Math.min(
            255,
            c.getGreen +
            line / 10
          ),
          Math.min(
            255,
            c.getBlue +
            line / 12
          )
        )
      )

      g.fillRect(
        sx,
        top + lineH / 3,
        Math.max(
          1,
          exx - sx
        ),
        Math.max(
          1,
          lineH / 12
        )
      )
    }

    r += 1
  }
}

def drawBillboard(
  g: Graphics2D,
  wx: Double,
  wy: Double,
  kind: String,
  size: Double,
  glow: Boolean
): Unit = {

  val dx =
    wx - px

  val dy =
    wy - py

  val d =
    Math.sqrt(
      dx * dx +
      dy * dy
    )

  val ang =
    angleNorm(
      Math.atan2(
        dy,
        dx
      ) - pa
    )

  if (
    Math.abs(ang) >
    HALF_FOV + 0.30
  ) {
    return
  }

  val depth =
    safeDepth(
      d *
      Math.cos(ang)
    )

  if (
    depth <= 0.15 ||
    depth >= MAX_DEPTH
  ) {
    return
  }

  val screenX =
    (
      GAME_W / 2.0
    ) +
    Math.tan(ang) *
    (
      GAME_W / 2.0
    ) /
    Math.tan(HALF_FOV)

  val h =
    clamp(
      size *
      VIEW_H.toDouble /
      depth,
      5.0,
      560.0
    )

  val w =
    h * 0.72

  val x =
    (
      screenX -
      w / 2.0
    ).toInt

  val y =
    (
      VIEW_H / 2.0 -
      h / 2.0
    ).toInt

  val centerRay =
    clamp(
      screenX /
      GAME_W.toDouble *
      RAYS.toDouble,
      0.0,
      (
        RAYS - 1
      ).toDouble
    ).toInt

  if (
    centerRay < 0 ||
    centerRay >= RAYS ||
    depth >
    zBuffer(centerRay) +
    0.25
  ) {
    return
  }

  if (glow) {

    g.setColor(
      new Color(
        235,
        220,
        120,
        90
      )
    )

    g.fillOval(
      (
        screenX -
        h * 0.25
      ).toInt,
      (
        VIEW_H / 2 -
        h * 0.25
      ).toInt,
      (
        h * 0.5
      ).toInt,
      (
        h * 0.5
      ).toInt
    )
  }

  if (
    kind == "ENEMY"
  ) {

    g.setColor(
      new Color(
        10,
        10,
        12,
        245
      )
    )

    g.fillOval(
      x +
      (
        w * 0.24
      ).toInt,
      y,
      (
        w * 0.52
      ).toInt,
      (
        h * 0.30
      ).toInt
    )

    g.fillRoundRect(
      x +
      (
        w * 0.18
      ).toInt,
      y +
      (
        h * 0.20
      ).toInt,
      (
        w * 0.64
      ).toInt,
      (
        h * 0.78
      ).toInt,
      22,
      22
    )

    g.setColor(
      new Color(
        215,
        215,
        205,
        230
      )
    )

    g.fillOval(
      x +
      (
        w * 0.34
      ).toInt,
      y +
      (
        h * 0.09
      ).toInt,
      (
        w * 0.11
      ).toInt,
      (
        w * 0.11
      ).toInt
    )

    g.fillOval(
      x +
      (
        w * 0.55
      ).toInt,
      y +
      (
        h * 0.09
      ).toInt,
      (
        w * 0.11
      ).toInt,
      (
        w * 0.11
      ).toInt
    )

    if (
      enemyState == "CHASE"
    ) {

      g.setColor(
        new Color(
          210,
          50,
          60
        )
      )

      g.fillOval(
        x +
        (
          w * 0.37
        ).toInt,
        y +
        (
          h * 0.15
        ).toInt,
        (
          w * 0.08
        ).toInt,
        (
          w * 0.05
        ).toInt
      )

      g.fillOval(
        x +
        (
          w * 0.54
        ).toInt,
        y +
        (
          h * 0.15
        ).toInt,
        (
          w * 0.08
        ).toInt,
        (
          w * 0.05
        ).toInt
      )
    }
  }
  else {

    val base =
      if (
        kind == "KEY"
      ) {
        new Color(
          232,
          192,
          75
        )
      }
      else if (
        kind == "FUSE"
      ) {
        new Color(
          232,
          86,
          75
        )
      }
      else if (
        kind == "CARD"
      ) {
        new Color(
          74,
          160,
          235
        )
      }
      else if (
        kind == "HANDLE"
      ) {
        new Color(
          210,
          145,
          60
        )
      }
      else if (
        kind == "BATTERY"
      ) {
        new Color(
          85,
          220,
          130
        )
      }
      else if (
        kind == "CROWBAR"
      ) {
        new Color(
          150,
          150,
          155
        )
      }
      else if (
        kind == "NOTE"
      ) {
        new Color(
          235,
          235,
          210
        )
      }
      else if (
        kind == "LAMP"
      ) {
        new Color(
          242,
          206,
          110
        )
      }
      else {
        new Color(
          120,
          200,
          220
        )
      }

    g.setColor(base)

    if (
      kind == "KEY"
    ) {

      g.fillOval(
        x,
        y + h.toInt / 3,
        (
          w * 0.36
        ).toInt,
        (
          w * 0.36
        ).toInt
      )

      g.fillRect(
        x +
        (
          w * 0.23
        ).toInt,
        y +
        h.toInt / 3 +
        (
          w * 0.10
        ).toInt,
        (
          w * 0.48
        ).toInt,
        (
          w * 0.10
        ).toInt
      )

      g.fillRect(
        x +
        (
          w * 0.53
        ).toInt,
        y +
        h.toInt / 3 +
        (
          w * 0.10
        ).toInt,
        (
          w * 0.10
        ).toInt,
        (
          w * 0.26
        ).toInt
      )
    }
    else if (
      kind == "CARD"
    ) {

      g.fillRoundRect(
        x,
        y,
        w.toInt,
        h.toInt,
        16,
        16
      )
    }
    else if (
      kind == "LAMP"
    ) {

      g.fillOval(
        x +
        (
          w * 0.18
        ).toInt,
        y +
        (
          h * 0.10
        ).toInt,
        (
          w * 0.64
        ).toInt,
        (
          h * 0.28
        ).toInt
      )

      g.fillRect(
        x +
        (
          w * 0.42
        ).toInt,
        y +
        (
          h * 0.34
        ).toInt,
        (
          w * 0.16
        ).toInt,
        (
          h * 0.50
        ).toInt
      )
    }
    else {

      g.fillRoundRect(
        x +
        (
          w * 0.18
        ).toInt,
        y,
        (
          w * 0.64
        ).toInt,
        h.toInt,
        12,
        12
      )
    }
  }
}

// ---------------------------------------------------------------------------
// WORLD OBJECTS / EFFECTS
// ---------------------------------------------------------------------------

def drawPerspectiveFloor(
  g: Graphics2D
): Unit = {

  val horizon =
    VIEW_H / 2 +
    cameraBobY.toInt

  g.setStroke(
    new BasicStroke(
      1.0f
    )
  )

  var i = 1

  while (
    i <= FLOOR_DETAIL
  ) {

    val t =
      i.toDouble /
      FLOOR_DETAIL.toDouble

    val y =
      (
        horizon +
        Math.pow(
          t,
          1.65
        ) *
        (
          VIEW_H -
          horizon
        )
      ).toInt

    val alpha =
      clamp(
        150.0 -
        i * 4.5,
        28.0,
        130.0
      ).toInt

    g.setColor(
      new Color(
        105,
        112,
        125,
        alpha
      )
    )

    g.drawLine(
      0,
      y,
      GAME_W,
      y
    )

    i += 1
  }

  var x = 0

  while (
    x <= GAME_W
  ) {

    val center =
      GAME_W /
      2.0

    val spread =
      (
        x - center
      ) /
      center

    val bottomX =
      x

    val topX =
      (
        center +
        spread *
        46.0
      ).toInt

    g.setColor(
      new Color(
        110,
        118,
        128,
        32
      )
    )

    g.drawLine(
      topX,
      horizon,
      bottomX,
      VIEW_H
    )

    x += 96
  }
}

def drawCeilingDetails(
  g: Graphics2D
): Unit = {

  val horizon =
    VIEW_H / 2 +
    cameraBobY.toInt

  var i = 0

  while (
    i < 9
  ) {

    val y =
      horizon -
      20 -
      i * 26

    val inset =
      i * 22

    g.setColor(
      new Color(
        26,
        28,
        35,
        95
      )
    )

    g.fillRect(
      inset,
      Math.max(
        0,
        y
      ),
      GAME_W -
      inset * 2,
      4
    )

    i += 1
  }
}

def drawAmbientVignette(
  g: Graphics2D
): Unit = {

  val a =
    clamp(
      55.0 +
      fear * 0.65 +
      (
        100.0 -
        battery
      ) *
      0.12,
      55.0,
      150.0
    ).toInt

  g.setColor(
    new Color(
      0,
      0,
      0,
      a
    )
  )

  g.fillRect(
    0,
    0,
    GAME_W,
    22
  )

  g.fillRect(
    0,
    VIEW_H - 22,
    GAME_W,
    22
  )

  g.fillRect(
    0,
    0,
    30,
    VIEW_H
  )

  g.fillRect(
    GAME_W - 30,
    0,
    30,
    VIEW_H
  )
}

def drawInteractPrompt(
  g: Graphics2D
): Unit = {

  if (
    state != "PLAYING" ||
    paused ||
    showHelp ||
    showBlueprint
  ) {
    return
  }

  var nearText = ""

  if (
    !keyFound &&
    dist(
      px,
      py,
      keyX,
      keyY
    ) < 1.45
  ) {

    nearText =
      "E  PICK UP BRASS KEY"
  }
  else if (
    !crowbarFound &&
    kitchenDoorOpen &&
    dist(
      px,
      py,
      crowbarX,
      crowbarY
    ) < 1.45
  ) {

    nearText =
      "E  TAKE CROWBAR"
  }
  else if (
    !fuseFound &&
    crowbarFound &&
    dist(
      px,
      py,
      fuseX,
      fuseY
    ) < 1.45
  ) {

    nearText =
      "E  TAKE EMERGENCY FUSE"
  }
  else if (
    !cardFound &&
    powerOn &&
    dist(
      px,
      py,
      cardX,
      cardY
    ) < 1.45
  ) {

    nearText =
      "E  TAKE ACCESS CARD"
  }
  else if (
    !noteFound &&
    powerOn &&
    dist(
      px,
      py,
      noteX,
      noteY
    ) < 1.45
  ) {

    nearText =
      "E  READ SECURITY NOTE"
  }
  else if (
    (
      !codeA &&
      dist(
        px,
        py,
        codeAX,
        codeAY
      ) < 1.45
    ) ||
    (
      !codeB &&
      dist(
        px,
        py,
        codeBX,
        codeBY
      ) < 1.45
    )
  ) {

    nearText =
      "E  SEARCH CODE FRAGMENT"
  }
  else if (
    !handleFound &&
    codeSolved &&
    cardFound &&
    dist(
      px,
      py,
      handleX,
      handleY
    ) < 1.45
  ) {

    nearText =
      "E  TAKE EXIT HANDLE"
  }
  else if (
    dist(
      px,
      py,
      generatorX,
      generatorY
    ) < 1.45 &&
    !powerOn
  ) {

    nearText =
      "E  USE GENERATOR"
  }
  else if (
    dist(
      px,
      py,
      securityX,
      securityY
    ) < 1.45 &&
    !codeSolved
  ) {

    nearText =
      "E  USE SECURITY TERMINAL"
  }
  else if (
    dist(
      px,
      py,
      exitX,
      exitY
    ) < 1.55
  ) {

    nearText =
      "E  CHECK FRONT GATE"
  }

  if (
    nearText != ""
  ) {

    val w = 360
    val x =
      (
        GAME_W - w
      ) / 2

    val y =
      VIEW_H - 118

    g.setColor(
      new Color(
        8,
        10,
        14,
        225
      )
    )

    g.fillRoundRect(
      x,
      y,
      w,
      42,
      14,
      14
    )

    g.setColor(
      new Color(
        230,
        235,
        242
      )
    )

    g.drawRoundRect(
      x,
      y,
      w,
      42,
      14,
      14
    )

    g.setFont(
      new Font(
        "SansSerif",
        Font.BOLD,
        17
      )
    )

    g.drawString(
      nearText,
      x + 28,
      y + 27
    )
  }
}

def drawWorldProps(
  g: Graphics2D
): Unit = {

  var i = 0

  while (
    i < propX.length
  ) {

    if (
      propX(i) >= 0.0
    ) {

      drawBillboard(
        g,
        propX(i),
        propY(i),
        propKind(i),
        0.95,
        false
      )
    }

    i += 1
  }

  if (!keyFound) {

    drawBillboard(
      g,
      keyX,
      keyY,
      "KEY",
      0.65,
      true
    )
  }

  if (
    !crowbarFound &&
    kitchenDoorOpen
  ) {

    drawBillboard(
      g,
      crowbarX,
      crowbarY,
      "CROWBAR",
      0.70,
      true
    )
  }

  if (
    !fuseFound &&
    crowbarFound
  ) {

    drawBillboard(
      g,
      fuseX,
      fuseY,
      "FUSE",
      0.62,
      true
    )
  }

  if (
    !cardFound &&
    powerOn
  ) {

    drawBillboard(
      g,
      cardX,
      cardY,
      "CARD",
      0.70,
      true
    )
  }

  if (
    !noteFound &&
    powerOn
  ) {

    drawBillboard(
      g,
      noteX,
      noteY,
      "NOTE",
      0.62,
      true
    )
  }

  if (
    !codeA &&
    powerOn
  ) {

    drawBillboard(
      g,
      codeAX,
      codeAY,
      "CODEA",
      0.60,
      true
    )
  }

  if (
    !codeB &&
    powerOn
  ) {

    drawBillboard(
      g,
      codeBX,
      codeBY,
      "CODEB",
      0.60,
      true
    )
  }

  if (
    !handleFound &&
    codeSolved &&
    cardFound
  ) {

    drawBillboard(
      g,
      handleX,
      handleY,
      "HANDLE",
      0.68,
      true
    )
  }

  if (!batteryFound) {

    drawBillboard(
      g,
      batteryX,
      batteryY,
      "BATTERY",
      0.65,
      true
    )
  }

  drawBillboard(
    g,
    ex,
    ey,
    "ENEMY",
    1.85,
    false
  )

  var li = 0

  while (
    li < lampX.length
  ) {

    drawBillboard(
      g,
      lampX(li),
      lampY(li),
      lampKind(li),
      0.55,
      true
    )

    li += 1
  }
}

def drawRain(
  g: Graphics2D
): Unit = {

  if (
    difficulty == "EASY"
  ) {
    return
  }

  var i = 0

  while (
    i < rainX.length
  ) {

    rainY(i) +=
      rainLen(i)

    rainX(i) -=
      2.2

    if (
      rainY(i) > VIEW_H ||
      rainX(i) < 0
    ) {

      rainY(i) =
        -20.0

      rainX(i) =
        Math.random() *
        GAME_W
    }

    g.setColor(
      new Color(
        155,
        180,
        210,
        85
      )
    )

    g.drawLine(
      rainX(i).toInt,
      rainY(i).toInt,
      (
        rainX(i) -
        6.0
      ).toInt,
      (
        rainY(i) +
        18.0
      ).toInt
    )

    i += 1
  }
}

def drawFlashlightOverlay(
  g: Graphics2D
): Unit = {

  if (
    !flashlightOn ||
    battery <= 0.0
  ) {

    g.setColor(
      new Color(
        0,
        0,
        0,
        95
      )
    )

    g.fillRect(
      0,
      0,
      GAME_W,
      VIEW_H
    )

    return
  }

  val cx =
    GAME_W / 2

  val cy =
    VIEW_H / 2

  val radius =
    (
      170 +
      Math.sin(
        animTick.toDouble *
        0.05
      ) *
      10
    ).toInt

  g.setColor(
    new Color(
      0,
      0,
      0,
      70
    )
  )

  g.fillRect(
    0,
    0,
    GAME_W,
    VIEW_H
  )

  g.setColor(
    new Color(
      255,
      250,
      215,
      18
    )
  )

  g.fillOval(
    cx -
    radius * 2,
    cy -
    radius,
    radius * 4,
    radius * 2
  )

  g.setColor(
    new Color(
      255,
      250,
      215,
      28
    )
  )

  g.fillOval(
    cx - radius,
    cy - radius / 2,
    radius * 2,
    radius
  )

  g.setColor(
    new Color(
      255,
      250,
      215,
      20
    )
  )

  g.fillOval(
    cx -
    radius / 2,
    cy -
    radius / 4,
    radius,
    radius / 2
  )

  val cone =
    new Polygon()

  cone.addPoint(
    cx - 110,
    cy + 25
  )

  cone.addPoint(
    cx + 110,
    cy + 25
  )

  cone.addPoint(
    cx + 270,
    VIEW_H
  )

  cone.addPoint(
    cx - 270,
    VIEW_H
  )

  g.setColor(
    new Color(
      255,
      248,
      215,
      12
    )
  )

  g.fillPolygon(cone)
}

def drawCrosshair(
  g: Graphics2D
): Unit = {

  val cx =
    GAME_W / 2

  val cy =
    VIEW_H / 2

  g.setColor(
    new Color(
      235,
      238,
      230,
      190
    )
  )

  g.drawLine(
    cx - 9,
    cy,
    cx - 2,
    cy
  )

  g.drawLine(
    cx + 2,
    cy,
    cx + 9,
    cy
  )

  g.drawLine(
    cx,
    cy - 9,
    cx,
    cy - 2
  )

  g.drawLine(
    cx,
    cy + 2,
    cx,
    cy + 9
  )
}

// ---------------------------------------------------------------------------
// MINIMAP / HUD
// ---------------------------------------------------------------------------

def drawMiniMap(
  g: Graphics2D
): Unit = {

  if (!showMap) {
    return
  }

  val mw = 300
  val mh = 205
  val ox = 20
  val oy = 18

  g.setColor(
    new Color(
      5,
      8,
      12,
      215
    )
  )

  g.fillRoundRect(
    ox,
    oy,
    mw,
    mh,
    16,
    16
  )

  val sx = 5.8
  val sy = 5.6

  var y = 0

  while (
    y < MAP_H
  ) {

    var x = 0

    while (
      x < MAP_W
    ) {

      val c =
        cellAt(
          x,
          y
        )

      if (
        c == '#'
      ) {

        g.setColor(
          new Color(
            95,
            100,
            112
          )
        )
      }
      else if (
        c == 'E'
      ) {

        g.setColor(
          new Color(
            220,
            65,
            70
          )
        )
      }
      else if (
        doorAt(x, y) &&
        !doorOpenAt(x, y)
      ) {

        g.setColor(
          new Color(
            177,
            122,
            66
          )
        )
      }
      else {

        g.setColor(
          new Color(
            24,
            30,
            38
          )
        )
      }

      g.fillRect(
        (
          ox +
          x * sx
        ).toInt,
        (
          oy +
          y * sy
        ).toInt,
        Math.max(
          2,
          sx.toInt
        ),
        Math.max(
          2,
          sy.toInt
        )
      )

      x += 1
    }

    y += 1
  }

  g.setColor(
    new Color(
      95,
      225,
      135
    )
  )

  g.fillOval(
    (
      ox +
      px * sx -
      4
    ).toInt,
    (
      oy +
      py * sy -
      4
    ).toInt,
    8,
    8
  )

  g.setColor(
    new Color(
      235,
      70,
      80
    )
  )

  g.fillOval(
    (
      ox +
      ex * sx -
      4
    ).toInt,
    (
      oy +
      ey * sy -
      4
    ).toInt,
    8,
    8
  )

  g.setColor(
    Color.WHITE
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      12
    )
  )

  g.drawString(
    "HOUSE MAP",
    ox + 12,
    oy + 17
  )
}

def drawBar(
  g: Graphics2D,
  x: Int,
  y: Int,
  w: Int,
  value: Double,
  max: Double,
  label: String,
  fill: Color
): Unit = {

  g.setColor(
    new Color(
      12,
      15,
      20,
      220
    )
  )

  g.fillRoundRect(
    x,
    y,
    w,
    20,
    8,
    8
  )

  g.setColor(fill)

  g.fillRoundRect(
    x,
    y,
    (
      w *
      clamp(
        value / max,
        0.0,
        1.0
      )
    ).toInt,
    20,
    8,
    8
  )

  g.setColor(
    Color.WHITE
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      12
    )
  )

  g.drawString(
    label,
    x + 8,
    y + 15
  )
}

def drawHud(
  g: Graphics2D
): Unit = {

  g.setColor(
    new Color(
      4,
      7,
      11,
      220
    )
  )

  g.fillRect(
    0,
    VIEW_H,
    GAME_W,
    GAME_H -
    VIEW_H
  )

  g.setColor(
    Color.WHITE
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      16
    )
  )

  g.drawString(
    "ROOM: " +
    roomNameAt(
      px,
      py
    ),
    20,
    VIEW_H + 26
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      13
    )
  )

  g.drawString(
    "DISCOVERED " +
    totalDiscoveries +
    "/" +
    roomNames.length,
    1040,
    VIEW_H + 26
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.PLAIN,
      14
    )
  )

  g.drawString(
    "TIME " +
    fmtTime(gameTime) +
    "    SCORE " +
    score,
    270,
    VIEW_H + 25
  )

  drawBar(
    g,
    20,
    VIEW_H + 40,
    220,
    health,
    100.0,
    "HEALTH",
    new Color(
      220,
      82,
      90
    )
  )

  drawBar(
    g,
    260,
    VIEW_H + 40,
    220,
    stamina,
    100.0,
    "STAMINA",
    new Color(
      90,
      178,
      235
    )
  )

  drawBar(
    g,
    500,
    VIEW_H + 40,
    220,
    battery,
    100.0,
    "BATTERY",
    new Color(
      95,
      220,
      145
    )
  )

  drawBar(
    g,
    740,
    VIEW_H + 40,
    220,
    fear,
    100.0,
    "FEAR",
    new Color(
      185,
      105,
      225
    )
  )

  g.setColor(
    new Color(
      225,
      232,
      240
    )
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.PLAIN,
      13
    )
  )

  g.drawString(
    "WASD/ARROWS move | SHIFT sprint | C crouch | mouse look | E interact | F flashlight | H hide | M map | B blueprint | P/ESC pause | F1 help",
    20,
    VIEW_H + 88
  )

  g.drawString(
    "KEY " +
    (
      if (keyFound)
        "OK"
      else
        "--"
    ) +
    "  CROWBAR " +
    (
      if (crowbarFound)
        "OK"
      else
        "--"
    ) +
    "  FUSE " +
    (
      if (fuseFound)
        "OK"
      else
        "--"
    ) +
    "  POWER " +
    (
      if (powerOn)
        "ON"
      else
        "OFF"
    ) +
    "  CARD " +
    (
      if (cardFound)
        "OK"
      else
        "--"
    ),
    20,
    VIEW_H + 111
  )

  g.drawString(
    "NOTE " +
    (
      if (noteFound)
        "OK"
      else
        "--"
    ) +
    "  CODE " +
    (
      if (codeSolved)
        "OPEN"
      else
        "LOCKED"
    ) +
    "  HANDLE " +
    (
      if (handleFound)
        "OK"
      else
        "--"
    ) +
    "  WATCHER " +
    enemyState +
    "  DIST " +
    fmt1(enemyDistance) +
    "  ROOMS " +
    totalDiscoveries +
    "/" +
    roomNames.length,
    590,
    VIEW_H + 111
  )

  g.setColor(
    new Color(
      255,
      235,
      185
    )
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      15
    )
  )

  g.drawString(
    currentObjective(),
    20,
    VIEW_H - 18
  )

  if (
    messageTicks > 0
  ) {

    g.setColor(
      new Color(
        255,
        245,
        210
      )
    )

    g.setFont(
      new Font(
        "SansSerif",
        Font.BOLD,
        17
      )
    )

    g.drawString(
      message,
      20,
      VIEW_H - 45
    )
  }

  if (hidden) {

    g.setColor(
      new Color(
        115,
        220,
        235
      )
    )

    g.setFont(
      new Font(
        "SansSerif",
        Font.BOLD,
        18
      )
    )

    g.drawString(
      "HIDDEN",
      1280,
      35
    )
  }
  else if (crouched) {

    g.setColor(
      new Color(
        160,
        220,
        205
      )
    )

    g.setFont(
      new Font(
        "SansSerif",
        Font.BOLD,
        16
      )
    )

    g.drawString(
      "CROUCH",
      1280,
      35
    )
  }

  if (!flashlightOn) {

    g.setColor(
      new Color(
        245,
        180,
        95
      )
    )

    g.setFont(
      new Font(
        "SansSerif",
        Font.BOLD,
        14
      )
    )

    g.drawString(
      "FLASHLIGHT OFF",
      1280,
      60
    )
  }
}

// ---------------------------------------------------------------------------
// OVERLAYS
// ---------------------------------------------------------------------------

def drawHelp(
  g: Graphics2D
): Unit = {

  if (!showHelp) {
    return
  }

  g.setColor(
    new Color(
      5,
      8,
      12,
      240
    )
  )

  g.fillRoundRect(
    220,
    70,
    1000,
    555,
    24,
    24
  )

  g.setColor(
    new Color(
      245,
      245,
      240
    )
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      32
    )
  )

  g.drawString(
    "HOW TO PLAY",
    560,
    115
  )

  val lines = Array(
    "Explore the original Old House and search every room.",
    "Find the key -> unlock the service door -> find the crowbar.",
    "Use the crowbar to reach the fuse -> restore the generator.",
    "Power enables the access card, security note and code fragments.",
    "Enter the code at the security terminal -> reach the hidden safe room.",
    "Take the exit handle and return to the front gate.",
    "The Watcher reacts to sight and noise. Sprinting is loud.",
    "Aim the flashlight at the Watcher to stun it for a short time.",
    "Hold C to crouch; crouching reduces movement noise and enemy sight range.",
    "Use H near furniture marked as a hiding zone.",
    "This is an original procedural 3D-style horror game; it does not reproduce a commercial game's map or assets."
  )

  g.setColor(
    new Color(
      230,
      234,
      240
    )
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.PLAIN,
      17
    )
  )

  var i = 0

  while (
    i < lines.length
  ) {

    g.drawString(
      lines(i),
      275,
      160 +
      i * 39
    )

    i += 1
  }
}

def drawBlueprint(
  g: Graphics2D
): Unit = {

  if (!showBlueprint) {
    return
  }

  g.setColor(
    new Color(
      4,
      6,
      10,
      245
    )
  )

  g.fillRoundRect(
    115,
    55,
    1210,
    625,
    22,
    22
  )

  g.setColor(
    new Color(
      240,
      242,
      250
    )
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      28
    )
  )

  g.drawString(
    "FULL HOUSE BLUEPRINT",
    565,
    95
  )

  val sx = 145.0
  val sy = 125.0
  val cw = 24.0
  val ch = 18.0

  var y = 0

  while (
    y < MAP_H
  ) {

    var x = 0

    while (
      x < MAP_W
    ) {

      val c =
        cellAt(
          x,
          y
        )

      if (
        c == '#'
      ) {

        g.setColor(
          new Color(
            84,
            92,
            108
          )
        )
      }
      else if (
        c == 'E'
      ) {

        g.setColor(
          new Color(
            220,
            70,
            78
          )
        )
      }
      else if (
        doorAt(x, y) &&
        !doorOpenAt(x, y)
      ) {

        g.setColor(
          new Color(
            190,
            130,
            70
          )
        )
      }
      else {

        g.setColor(
          new Color(
            24,
            31,
            42
          )
        )
      }

      g.fillRect(
        (
          sx +
          x * cw
        ).toInt,
        (
          sy +
          y * ch
        ).toInt,
        cw.toInt - 1,
        ch.toInt - 1
      )

      x += 1
    }

    y += 1
  }

  g.setColor(
    new Color(
      100,
      230,
      135
    )
  )

  g.fillOval(
    (
      sx +
      px * cw -
      5
    ).toInt,
    (
      sy +
      py * ch -
      5
    ).toInt,
    10,
    10
  )

  g.setColor(
    new Color(
      240,
      70,
      80
    )
  )

  g.fillOval(
    (
      sx +
      ex * cw -
      5
    ).toInt,
    (
      sy +
      ey * ch -
      5
    ).toInt,
    10,
    10
  )

  g.setColor(
    Color.WHITE
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.PLAIN,
      15
    )
  )

  g.drawString(
    "Green player | Red Watcher | Brown locked door | Red gate",
    165,
    655
  )
}

def drawPause(
  g: Graphics2D
): Unit = {

  if (!paused) {
    return
  }

  g.setColor(
    new Color(
      0,
      0,
      0,
      180
    )
  )

  g.fillRect(
    0,
    0,
    GAME_W,
    GAME_H
  )

  g.setColor(
    Color.WHITE
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      54
    )
  )

  g.drawString(
    "PAUSED",
    590,
    300
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.PLAIN,
      18
    )
  )

  g.drawString(
    "Press P or ESC to resume",
    555,
    340
  )
}

def drawScare(
  g: Graphics2D
): Unit = {

  if (
    scareFlash <= 0
  ) {
    return
  }

  g.setColor(
    new Color(
      130,
      0,
      15,
      Math.min(
        205,
        scareFlash * 10
      )
    )
  )

  g.fillRect(
    0,
    0,
    GAME_W,
    VIEW_H
  )
}

// ---------------------------------------------------------------------------
// MENU / END
// ---------------------------------------------------------------------------

def drawMenu(
  g: Graphics2D
): Unit = {

  g.setColor(
    new Color(
      3,
      5,
      8
    )
  )

  g.fillRect(
    0,
    0,
    GAME_W,
    GAME_H
  )

  g.setColor(
    new Color(
      16,
      19,
      26
    )
  )

  g.fillRect(
    300,
    120,
    840,
    420
  )

  val roof =
    new Polygon()

  roof.addPoint(
    240,
    150
  )

  roof.addPoint(
    720,
    60
  )

  roof.addPoint(
    1200,
    150
  )

  g.setColor(
    new Color(
      9,
      11,
      16
    )
  )

  g.fillPolygon(roof)

  g.setColor(
    new Color(
      54,
      58,
      66
    )
  )

  g.fillRoundRect(
    630,
    300,
    180,
    240,
    18,
    18
  )

  g.setColor(
    new Color(
      219,
      66,
      70,
      150
    )
  )

  g.fillOval(
    705,
    380,
    30,
    30
  )

  g.setColor(
    new Color(
      235,
      238,
      242
    )
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      56
    )
  )

  g.drawString(
    "OLD HOUSE // OMEGA V5",
    455,
    110
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      22
    )
  )

  g.drawString(
    "NIGHT WATCH // ADVANCED SOFTWARE 3D HORROR ESCAPE",
    505,
    150
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.PLAIN,
      15
    )
  )

  g.setColor(
    new Color(
      180,
      186,
      196
    )
  )

  g.drawString(
    "Explore every room, solve layered puzzles, manage noise, and survive the Watcher.",
    505,
    177
  )

  val labels = Array(
    "START NIGHT",
    "HOW TO PLAY",
    "QUIT"
  )

  var i = 0

  while (
    i < labels.length
  ) {

    val r =
      new Rectangle(
        510,
        560 + i * 64,
        420,
        48
      )

    g.setColor(
      new Color(
        22,
        26,
        34,
        240
      )
    )

    g.fillRoundRect(
      r.x,
      r.y,
      r.width,
      r.height,
      14,
      14
    )

    g.setColor(
      new Color(
        125,
        135,
        150
      )
    )

    g.drawRoundRect(
      r.x,
      r.y,
      r.width,
      r.height,
      14,
      14
    )

    g.setColor(
      Color.WHITE
    )

    g.setFont(
      new Font(
        "SansSerif",
        Font.BOLD,
        19
      )
    )

    g.drawString(
      labels(i),
      r.x + 145,
      r.y + 31
    )

    i += 1
  }

  g.setColor(
    new Color(
      208,
      215,
      225
    )
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      15
    )
  )

  g.drawString(
    "Difficulty",
    545,
    775
  )

  val diffs = Array(
    "EASY",
    "NORMAL",
    "HARD"
  )

  var d = 0

  while (
    d < diffs.length
  ) {

    val rr =
      new Rectangle(
        680 +
        d * 120,
        750,
        100,
        38
      )

    if (
      diffs(d) ==
      difficulty
    ) {

      g.setColor(
        new Color(
          78,
          145,
          105
        )
      )
    }
    else {

      g.setColor(
        new Color(
          40,
          46,
          58
        )
      )
    }

    g.fillRoundRect(
      rr.x,
      rr.y,
      rr.width,
      rr.height,
      10,
      10
    )

    g.setColor(
      Color.WHITE
    )

    g.drawRoundRect(
      rr.x,
      rr.y,
      rr.width,
      rr.height,
      10,
      10
    )

    g.drawString(
      diffs(d),
      rr.x + 22,
      rr.y + 25
    )

    d += 1
  }

  g.setColor(
    new Color(
      160,
      170,
      184
    )
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.PLAIN,
      13
    )
  )

  g.drawString(
    "Original advanced software 3D renderer using Java2D / Swing | Mouse look | Dynamic lighting | AI",
    360,
    820
  )

  if (showHelp) {
    drawHelp(g)
  }
}

def drawEnd(
  g: Graphics2D,
  won: Boolean
): Unit = {

  g.setColor(
    if (won)
      new Color(
        5,
        28,
        18
      )
    else
      new Color(
        28,
        6,
        10
      )
  )

  g.fillRect(
    0,
    0,
    GAME_W,
    GAME_H
  )

  g.setColor(
    Color.WHITE
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      54
    )
  )

  g.drawString(
    if (won)
      "YOU ESCAPED"
    else
      "THE NIGHT ENDED",
    485,
    215
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.PLAIN,
      21
    )
  )

  g.drawString(
    "Time: " +
    fmtTime(gameTime),
    610,
    275
  )

  g.drawString(
    "Score: " +
    score,
    610,
    310
  )

  g.drawString(
    "Best: " +
    (
      if (bestTime == 0.0)
        "--"
      else
        fmtTime(bestTime)
    ),
    610,
    345
  )

  g.drawString(
    if (won)
      "All objectives completed."
    else
      "The Watcher reached you.",
    535,
    390
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.PLAIN,
      14
    )
  )

  g.drawString(
    "Original game design. No commercial game's map or assets are reproduced.",
    490,
    420
  )

  g.setFont(
    new Font(
      "SansSerif",
      Font.BOLD,
      18
    )
  )

  g.drawString(
    "R = restart    ENTER = restart    M = main menu",
    520,
    455
  )
}

// ---------------------------------------------------------------------------
// EXTRA EFFECTS
// ---------------------------------------------------------------------------

def drawLightBeams(
  g: Graphics2D
): Unit = {

  if (
    !flashlightOn ||
    battery <= 0.0 ||
    paused
  ) {
    return
  }

  val cx =
    GAME_W / 2 +
    cameraBobX.toInt

  val cy =
    VIEW_H / 2 +
    cameraBobY.toInt

  val alpha =
    (
      22.0 +
      38.0 *
      (
        battery /
        100.0
      ) -
      flashlightFlicker *
      12.0
    ).toInt

  val p =
    new Polygon()

  p.addPoint(
    cx - 82,
    cy + 18
  )

  p.addPoint(
    cx + 82,
    cy + 18
  )

  p.addPoint(
    cx + 360,
    VIEW_H
  )

  p.addPoint(
    cx - 360,
    VIEW_H
  )

  g.setColor(
    new Color(
      255,
      248,
      220,
      Math.max(
        8,
        Math.min(
          68,
          alpha
        )
      )
    )
  )

  g.fillPolygon(p)

  var ring = 0

  while (
    ring < 4
  ) {

    val w =
      160 +
      ring * 120

    val h =
      24 +
      ring * 20

    g.setColor(
      new Color(
        255,
        250,
        230,
        Math.max(
          3,
          alpha /
          (ring + 2)
        )
      )
    )

    g.drawOval(
      cx - w,
      cy - h,
      w * 2,
      h * 2
    )

    ring += 1
  }
}

def drawWallSeams(
  g: Graphics2D
): Unit = {

  var k = 0

  while (
    k < RAYS
  ) {

    if (
      k % 18 == 0
    ) {

      val d =
        zBuffer(k)

      if (
        d <
        MAX_DEPTH - 0.2
      ) {

        val h =
          clamp(
            VIEW_H.toDouble /
            safeDepth(d),
            6.0,
            VIEW_H.toDouble *
            1.5
          ).toInt

        val top =
          VIEW_H / 2 -
          h / 2

        val x =
          (
            k.toDouble *
            GAME_W.toDouble /
            RAYS.toDouble
          ).toInt

        g.setColor(
          new Color(
            5,
            7,
            10,
            60
          )
        )

        g.drawLine(
          x,
          top,
          x,
          Math.min(
            VIEW_H,
            top + h
          )
        )
      }
    }

    k += 1
  }
}

def drawHouseAtmosphere(
  g: Graphics2D
): Unit = {

  var i = 0

  while (
    i < dustX.length
  ) {

    val phase =
      ambientPulse *
      (
        0.35 +
        (
          i % 5
        ) *
        0.03
      )

    val xx =
      (
        (
          dustX(i) +
          Math.sin(
            phase + i
          ) *
          18.0
        ) %
        GAME_W +
        GAME_W
      ) %
      GAME_W

    val yy =
      (
        (
          dustY(i) +
          Math.cos(
            phase *
            0.8 +
            i
          ) *
          8.0
        ) %
        VIEW_H +
        VIEW_H
      ) %
      VIEW_H

    val a =
      18 +
      (
        (
          i * 7
        ) %
        20
      )

    g.setColor(
      new Color(
        210,
        205,
        190,
        a
      )
    )

    g.fillOval(
      xx.toInt,
      yy.toInt,
      2 +
      (
        i % 3
      ),
      2 +
      (
        i % 3
      )
    )

    i += 1
  }
}

def drawRoomDecor(
  g: Graphics2D
): Unit = {

  val baseY =
    VIEW_H / 2 +
    cameraBobY.toInt

  val pulse =
    (
      Math.sin(
        ambientPulse *
        0.9
      ) +
      1.0
    ) *
    0.5

  if (powerOn) {

    var i = 0

    while (
      i < 6
    ) {

      val x =
        130 +
        i * 230

      g.setColor(
        new Color(
          248,
          205,
          105,
          (
            30 +
            22 *
            pulse
          ).toInt
        )
      )

      g.fillOval(
        x - 28,
        Math.max(
          26,
          baseY -
          210 -
          (
            i % 2
          ) *
          32
        ),
        56,
        26
      )

      g.setColor(
        new Color(
          120,
          100,
          58,
          150
        )
      )

      g.fillRect(
        x - 4,
        Math.max(
          48,
          baseY -
          192 -
          (
            i % 2
          ) *
          32
        ),
        8,
        28
      )

      i += 1
    }
  }

  var j = 0

  while (
    j < 5
  ) {

    val x =
      170 +
      j * 270

    val y =
      baseY -
      120 -
      (
        j % 2
      ) *
      45

    g.setColor(
      new Color(
        24,
        26,
        32,
        120
      )
    )

    g.fillRect(
      x,
      y,
      110,
      7
    )

    g.fillRect(
      x + 8,
      y - 36,
      7,
      36
    )

    g.fillRect(
      x + 102,
      y - 28,
      7,
      28
    )

    g.setColor(
      new Color(
        55,
        45,
        38,
        130
      )
    )

    g.drawRect(
      x + 18,
      y - 27,
      70,
      18
    )

    j += 1
  }
}

def drawPlayerHands(
  g: Graphics2D
): Unit = {

  val bob =
    Math.sin(
      headBob
    ) *
    4.0

  val base =
    VIEW_H +
    8 +
    bob.toInt

  val cx =
    GAME_W / 2

  g.setColor(
    new Color(
      70,
      72,
      76,
      235
    )
  )

  g.fillRoundRect(
    cx - 150,
    base - 58,
    72,
    110,
    24,
    24
  )

  g.fillRoundRect(
    cx + 78,
    base - 58,
    72,
    110,
    24,
    24
  )

  g.setColor(
    new Color(
      150,
      144,
      132,
      230
    )
  )

  g.fillOval(
    cx - 132,
    base - 18,
    36,
    50
  )

  g.fillOval(
    cx + 96,
    base - 18,
    36,
    50
  )

  g.setColor(
    new Color(
      34,
      34,
      38,
      245
    )
  )

  g.fillRoundRect(
    cx - 72,
    base - 72,
    144,
    32,
    12,
    12
  )

  g.setColor(
    new Color(
      220,
      210,
      180,
      200
    )
  )

  g.drawRoundRect(
    cx - 72,
    base - 72,
    144,
    32,
    12,
    12
  )
}

def drawInteractionReticle(
  g: Graphics2D
): Unit = {

  if (
    interactionPulse <= 0.0
  ) {
    return
  }

  val r =
    (
      18.0 +
      interactionPulse
    ).toInt

  g.setColor(
    new Color(
      245,
      215,
      125,
      Math.min(
        180,
        (
          interactionPulse *
          12.0
        ).toInt
      )
    )
  )

  g.drawOval(
    GAME_W / 2 - r,
    VIEW_H / 2 - r,
    r * 2,
    r * 2
  )
}

def drawThreatEdge(
  g: Graphics2D
): Unit = {

  if (
    enemyState != "CHASE" ||
    hidden
  ) {
    return
  }

  val a =
    clamp(
      (
        9.0 -
        enemyDistance
      ) *
      17.0,
      18.0,
      120.0
    ).toInt

  g.setColor(
    new Color(
      155,
      22,
      30,
      a
    )
  )

  g.fillRect(
    0,
    0,
    18,
    VIEW_H
  )

  g.fillRect(
    GAME_W - 18,
    0,
    18,
    VIEW_H
  )

  g.fillRect(
    0,
    0,
    GAME_W,
    12
  )

  g.fillRect(
    0,
    VIEW_H - 12,
    GAME_W,
    12
  )
}

def drawScreenGrain(
  g: Graphics2D
): Unit = {

  if (
    screenGrain <= 0.0
  ) {
    return
  }

  var i = 0

  while (
    i < 70
  ) {

    val x =
      (
        (
          i * 137 +
          animTick.toInt *
          3
        ) %
        GAME_W
      )

    val y =
      (
        (
          i * 61 +
          animTick.toInt *
          5
        ) %
        VIEW_H
      )

    val a =
      Math.min(
        45,
        screenGrain.toInt +
        (
          i % 12
        )
      )

    g.setColor(
      new Color(
        220,
        220,
        225,
        a
      )
    )

    g.fillRect(
      x,
      y,
      1,
      1
    )

    i += 1
  }
}

def drawPlaying(
  g: Graphics2D
): Unit = {

  val oldTx =
    g.getTransform

  g.translate(
    (
      Math.sin(cameraRoll) *
      5.0
    ).toInt,
    (
      Math.cos(cameraRoll) *
      2.0
    ).toInt
  )

  drawWallView(g)
  drawWallSeams(g)
  drawHouseAtmosphere(g)
  drawRoomDecor(g)
  drawWorldProps(g)
  drawLightBeams(g)
  drawFlashlightOverlay(g)
  drawRain(g)
  drawCrosshair(g)
  drawInteractPrompt(g)
  drawMiniMap(g)
  drawHud(g)
  drawPause(g)
  drawHelp(g)
  drawBlueprint(g)
  drawAmbientVignette(g)
  drawThreatEdge(g)
  drawInteractionReticle(g)
  drawScreenGrain(g)
  drawPlayerHands(g)
  drawScare(g)

  g.setTransform(
    oldTx
  )
}

// ---------------------------------------------------------------------------
// MENU CLICK HELPERS
// ---------------------------------------------------------------------------

def handleMenuClick(
  x: Int,
  y: Int
): Unit = {

  if (
    state == "MENU"
  ) {

    if (
      y >= 550 &&
      y < 610
    ) {

      beginGame()
    }
    else if (
      y >= 614 &&
      y < 675
    ) {

      showHelp =
        !showHelp
    }
    else if (
      y >= 678 &&
      y < 738
    ) {

      frame.dispose()
    }

    if (
      y >= 748 &&
      y <= 795
    ) {

      if (
        x >= 680 &&
        x < 780
      ) {

        setDifficulty(
          "EASY"
        )
      }
      else if (
        x >= 800 &&
        x < 900
      ) {

        setDifficulty(
          "NORMAL"
        )
      }
      else if (
        x >= 920 &&
        x < 1020
      ) {

        setDifficulty(
          "HARD"
        )
      }
    }
  }
  else if (
    (
      state == "WIN" ||
      state == "LOSE"
    ) &&
    y > 420 &&
    y < 490
  ) {

    beginGame()
  }
}

// ---------------------------------------------------------------------------
// PANEL / INPUT
// ---------------------------------------------------------------------------

val panel =
  new JPanel {

    setFocusable(true)

    override def paintComponent(
      graphics: Graphics
    ): Unit = {

      super.paintComponent(
        graphics
      )

      val g =
        graphics.asInstanceOf[
          Graphics2D
        ]

      g.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON
      )

      g.setRenderingHint(
        RenderingHints.KEY_RENDERING,
        RenderingHints.VALUE_RENDER_SPEED
      )

      if (
        state == "MENU"
      ) {

        drawMenu(g)
      }
      else if (
        state == "PLAYING"
      ) {

        drawPlaying(g)
      }
      else if (
        state == "WIN"
      ) {

        drawEnd(
          g,
          true
        )
      }
      else if (
        state == "LOSE"
      ) {

        drawEnd(
          g,
          false
        )
      }
    }

    addKeyListener(
      new KeyAdapter {

        override def keyPressed(
          e: KeyEvent
        ): Unit = {

          val c =
            e.getKeyCode

          if (
            c >= 0 &&
            c < keys.length
          ) {

            keys(c) =
              true
          }

          if (
            c == KeyEvent.VK_ESCAPE &&
            state == "PLAYING"
          ) {

            paused =
              !paused

            showHelp =
              false

            showBlueprint =
              false
          }

          if (
            c == KeyEvent.VK_P &&
            state == "PLAYING"
          ) {

            paused =
              !paused
          }

          if (
            c == KeyEvent.VK_F &&
            state == "PLAYING" &&
            !paused
          ) {

            flashlightOn =
              !flashlightOn
          }

          if (
            c == KeyEvent.VK_H &&
            state == "PLAYING" &&
            !paused
          ) {

            toggleHide()
          }

          if (
            c == KeyEvent.VK_E &&
            state == "PLAYING" &&
            !paused
          ) {

            interact()
          }

          if (
            c == KeyEvent.VK_M &&
            state == "PLAYING"
          ) {

            showMap =
              !showMap
          }

          if (
            c == KeyEvent.VK_B &&
            state == "PLAYING"
          ) {

            showBlueprint =
              !showBlueprint
          }

          if (
            c == KeyEvent.VK_F1
          ) {

            showHelp =
              !showHelp
          }

          if (
            c == KeyEvent.VK_F6
          ) {

            muted =
              !muted
          }

          if (
            c == KeyEvent.VK_ENTER &&
            state == "MENU"
          ) {

            beginGame()
          }

          if (
            (
              c == KeyEvent.VK_R ||
              c == KeyEvent.VK_ENTER
            ) &&
            (
              state == "WIN" ||
              state == "LOSE"
            )
          ) {

            beginGame()
          }

          if (
            c == KeyEvent.VK_M &&
            (
              state == "WIN" ||
              state == "LOSE"
            )
          ) {

            state =
              "MENU"
          }

          // FIX: repaint() directly
          repaint()
        }

        override def keyReleased(
          e: KeyEvent
        ): Unit = {

          val c =
            e.getKeyCode

          if (
            c >= 0 &&
            c < keys.length
          ) {

            keys(c) =
              false
          }
        }
      }
    )

    addMouseListener(
      new MouseAdapter {

        override def mousePressed(
          e: MouseEvent
        ): Unit = {

          handleMenuClick(
            e.getX,
            e.getY
          )

          mouseLastX =
            e.getX

          // FIX: mouseDown is var
          
            true

          requestFocusInWindow()

          repaint()
        }

        override def mouseReleased(
          e: MouseEvent
        ): Unit = {

          // FIX: mouseDown is var
          
            false
        }
      }
    )

    addMouseWheelListener(
      new MouseWheelListener {

        override def mouseWheelMoved(
          e: MouseWheelEvent
        ): Unit = {

          if (
            state == "PLAYING" &&
            !paused &&
            !showHelp &&
            !showBlueprint
          ) {

            if (
              e.getWheelRotation < 0
            ) {

              setMessage(
                "Mouse wheel: look sensitivity focus. Use mouse drag for camera.",
                70
              )
            }
            else {

              setMessage(
                "Mouse wheel: use drag to turn. C = crouch.",
                70
              )
            }

            interactionPulse =
              12.0
          }

          repaint()
        }
      }
    )

    addMouseMotionListener(
      new MouseMotionAdapter {

        override def mouseDragged(
          e: MouseEvent
        ): Unit = {

          if (
            state == "PLAYING" &&
            !paused &&
            !showHelp &&
            !showBlueprint
          ) {

            pa =
              angleNorm(
                pa +
                (
                  e.getX -
                  mouseLastX
                ).toDouble *
                MOUSE_SENS
              )

            mouseLastX =
              e.getX
          }
        }
      }
    )
  }

// ---------------------------------------------------------------------------
// PARTICLE INITIALIZATION
// ---------------------------------------------------------------------------

var iRain = 0

while (
  iRain < rainX.length
) {

  rainX(iRain) =
    Math.random() *
    GAME_W

  rainY(iRain) =
    Math.random() *
    VIEW_H

  rainLen(iRain) =
    6.0 +
    Math.random() *
    10.0

  iRain += 1
}

var iDust = 0

while (
  iDust < dustX.length
) {

  dustX(iDust) =
    Math.random() *
    GAME_W

  dustY(iDust) =
    Math.random() *
    VIEW_H

  iDust += 1
}

// ---------------------------------------------------------------------------
// GAME TIMER
// ---------------------------------------------------------------------------

val timer =
  new javax.swing.Timer(
    16,
    new ActionListener {

      override def actionPerformed(
        e: ActionEvent
      ): Unit = {

        animTick += 1L

        if (
          state == "PLAYING" &&
          !paused &&
          running
        ) {

          gameTime +=
            0.016

          if (
            messageTicks > 0
          ) {

            messageTicks -= 1
          }

          if (
            doorPulse > 0
          ) {

            doorPulse -= 1
          }

          if (
            exitGlow < 30 &&
            finalGateOpen
          ) {

            exitGlow += 1
          }

          if (
            interactionPulse > 0.0
          ) {

            interactionPulse -= 1.0
          }

          ambientPulse +=
            0.045

          flashlightFlicker =
            if (
              battery < 12.0 &&
              flashlightOn
            ) {

              (
                Math.sin(
                  animTick.toDouble *
                  0.65
                ) +
                1.0
              ) *
              0.5
            }
            else {
              0.0
            }

          // FIX:
          // Do NOT use local 'sprinting' here.
          // Use global sprintingNow.
          cameraRoll =
            Math.sin(
              headBob *
              0.45
            ) *
            (
              if (sprintingNow)
                0.009
              else
                0.004
            )

          screenGrain =
            clamp(
              screenGrain +
              (
                if (
                  fear > 65.0
                )
                  0.8
                else
                  -0.45
              ),
              0.0,
              18.0
            )

          if (
            Math.random() <
            0.0018
          ) {

            lightning = 7
          }

          if (
            lightning > 0
          ) {

            lightning -= 1
          }

          updatePlayer(
            0.016
          )

          updateEnemy(
            0.016
          )

          if (
            scareFlash > 0
          ) {

            scareFlash -= 1
          }

          if (
            enemyState == "CHASE" &&
            animTick % 70L == 0L
          ) {

            safeTone()
          }
        }

        panel.repaint()
      }
    }
  )

// ---------------------------------------------------------------------------
// START APPLICATION
// ---------------------------------------------------------------------------

SwingUtilities.invokeLater(
  new Runnable {

    def run(): Unit = {

      frame.setContentPane(
        panel
      )

      frame.setVisible(
        true
      )

      panel.requestFocusInWindow()

      timer.start()
    }
  }
)