Code Sketch


yoiiiiiii
By: Mhalsakant School
Category: Programming
import javax.swing._
import java.awt._
import java.awt.event._
import java.awt.geom._
import java.util.Random
import scala.collection.mutable.ArrayBuffer

// ================================================================
// NIGHTFALL: HORROR SURVIVAL
// Original Java2D horror battle-royale style game
// No external assets required
// ================================================================

val GAME_W = 1440
val GAME_H = 860
val HUD_H = 90
val WORLD_W = GAME_W
val WORLD_H = GAME_H - HUD_H

val PLAYER_RADIUS = 17.0
val PLAYER_SPEED = 4.2
val SPRINT_SPEED = 6.8
val BULLET_SPEED = 15.0
val ENEMY_SPEED = 1.65
val MAX_AMMO = 30
val MAX_HP = 100.0
val MAX_ARMOR = 100.0
val ZONE_START = 600.0
val ZONE_MIN = 105.0

val frame = new JFrame("NIGHTFALL : HORROR SURVIVAL")
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.setSize(GAME_W, GAME_H)
frame.setResizable(false)
frame.setLocationRelativeTo(null)

// ================================================================
// GAME STATE
// ================================================================

var gameState = "MENU"
var difficulty = "NORMAL"
var paused = false
var running = true

var playerX = WORLD_W / 2.0
var playerY = WORLD_H / 2.0
var playerAngle = 0.0

var playerHP = MAX_HP
var playerArmor = 25.0
var stamina = 100.0

var ammo = MAX_AMMO
var reserveAmmo = 120
var medkits = 2

var kills = 0
var aliveEnemies = 18
var survivalTime = 0.0

var zoneRadius = ZONE_START
var zoneCenterX = WORLD_W / 2.0
var zoneCenterY = WORLD_H / 2.0

var mouseX = WORLD_W / 2
var mouseY = WORLD_H / 2
var mousePressedNow = false

var lastShotTick = 0L
var reloadTicks = 0
var reloadMax = 0

var flashTicks = 0
var damageFlash = 0
var screenShake = 0.0
var message = "SURVIVE THE NIGHT"
var messageTicks = 240

var animTick = 0L
var lastDamageTick = -100L

var showMap = true
var showHelp = false

var difficultyEnemyHealth = 1.0
var difficultyEnemyDamage = 1.0
var difficultyEnemySpeed = 1.0
var difficultyZoneDamage = 1.0

var random = new Random()

// ================================================================
// DATA TYPES
// ================================================================

case class Bullet(
  var x: Double,
  var y: Double,
  var dx: Double,
  var dy: Double,
  var life: Int,
  var fromPlayer: Boolean,
  var damage: Double
)

case class Enemy(
  var x: Double,
  var y: Double,
  var angle: Double,
  var hp: Double,
  var maxHp: Double,
  var speed: Double,
  var cooldown: Int,
  var kind: String,
  var state: String,
  var fear: Double
)

case class Loot(
  var x: Double,
  var y: Double,
  var kind: String,
  var amount: Int,
  var taken: Boolean
)

case class Wall(
  x: Int,
  y: Int,
  w: Int,
  h: Int
)

// ================================================================
// COLLECTIONS
// ================================================================

val bullets = ArrayBuffer[Bullet]()
val enemies = ArrayBuffer[Enemy]()
val loot = ArrayBuffer[Loot]()
val walls = ArrayBuffer[Wall]()

// ================================================================
// BASIC HELPERS
// ================================================================

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

def dist(
  x1: Double,
  y1: Double,
  x2: Double,
  y2: Double
): Double = {
  val dx = x1 - x2
  val dy = y1 - y2
  Math.sqrt(
    dx * dx +
    dy * dy
  )
}

def normAngle(
  a: Double
): Double = {
  var v = a
  while (v > Math.PI) {
    v -= Math.PI * 2.0
  }
  while (v < -Math.PI) {
    v += Math.PI * 2.0
  }
  v
}

def say(
  s: String,
  ticks: Int
): Unit = {
  message = s
  messageTicks = ticks
}

def randomDouble(
  min: Double,
  max: Double
): Double = {
  min +
  random.nextDouble() *
  (max - min)
}

def randomInt(
  min: Int,
  max: Int
): Int = {
  min +
  random.nextInt(
    Math.max(
      1,
      max - min + 1
    )
  )
}

// ================================================================
// DIFFICULTY
// ================================================================

def setDifficulty(
  d: String
): Unit = {

  difficulty = d

  if (d == "EASY") {

    difficultyEnemyHealth = 0.80
    difficultyEnemyDamage = 0.65
    difficultyEnemySpeed = 0.78
    difficultyZoneDamage = 0.65
  }
  else if (d == "HARD") {

    difficultyEnemyHealth = 1.45
    difficultyEnemyDamage = 1.45
    difficultyEnemySpeed = 1.28
    difficultyZoneDamage = 1.40
  }
  else {

    difficultyEnemyHealth = 1.0
    difficultyEnemyDamage = 1.0
    difficultyEnemySpeed = 1.0
    difficultyZoneDamage = 1.0
  }
}

// ================================================================
// COLLISION
// ================================================================

def circleRectCollision(
  cx: Double,
  cy: Double,
  r: Double,
  wall: Wall
): Boolean = {

  val nearestX =
    clamp(
      cx,
      wall.x.toDouble,
      (wall.x + wall.w).toDouble
    )

  val nearestY =
    clamp(
      cy,
      wall.y.toDouble,
      (wall.y + wall.h).toDouble
    )

  val dx =
    cx - nearestX

  val dy =
    cy - nearestY

  dx * dx +
  dy * dy <
  r * r
}

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

  if (
    x < radius ||
    y < radius ||
    x > WORLD_W - radius ||
    y > WORLD_H - radius
  ) {
    return true
  }

  var i = 0

  while (
    i < walls.length
  ) {

    if (
      circleRectCollision(
        x,
        y,
        radius,
        walls(i)
      )
    ) {
      return true
    }

    i += 1
  }

  false
}

def moveObject(
  oldX: Double,
  oldY: Double,
  newX: Double,
  newY: Double,
  radius: Double
): (Double, Double) = {

  var nx = newX
  var ny = newY

  if (
    blocked(
      nx,
      oldY,
      radius
    )
  ) {
    nx = oldX
  }

  if (
    blocked(
      nx,
      ny,
      radius
    )
  ) {
    ny = oldY
  }

  (
    nx,
    ny
  )
}

def lineBlocked(
  x1: Double,
  y1: Double,
  x2: Double,
  y2: Double
): Boolean = {

  val d =
    dist(
      x1,
      y1,
      x2,
      y2
    )

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

  var i = 1

  while (
    i < steps
  ) {

    val t =
      i.toDouble /
      steps.toDouble

    val x =
      x1 +
      (
        x2 -
        x1
      ) *
      t

    val y =
      y1 +
      (
        y2 -
        y1
      ) *
      t

    if (
      blocked(
        x,
        y,
        2.0
      )
    ) {
      return true
    }

    i += 1
  }

  false
}

// ================================================================
// WORLD GENERATION
// ================================================================

def buildWorld(): Unit = {

  walls.clear()
  loot.clear()
  enemies.clear()
  bullets.clear()

  // Outer atmosphere walls
  walls += Wall(
    90,
    90,
    1260,
    28
  )

  walls += Wall(
    90,
    WORLD_H - 118,
    1260,
    28
  )

  walls += Wall(
    90,
    90,
    28,
    610
  )

  walls += Wall(
    1322,
    90,
    28,
    610
  )

  // Large abandoned-house structures
  walls += Wall(
    250,
    160,
    220,
    35
  )

  walls += Wall(
    250,
    160,
    35,
    180
  )

  walls += Wall(
    435,
    160,
    35,
    180
  )

  walls += Wall(
    970,
    160,
    220,
    35
  )

  walls += Wall(
    1155,
    160,
    35,
    180
  )

  walls += Wall(
    970,
    305,
    220,
    35
  )

  walls += Wall(
    250,
    450,
    300,
    35
  )

  walls += Wall(
    250,
    450,
    35,
    170
  )

  walls += Wall(
    515,
    450,
    35,
    170
  )

  walls += Wall(
    900,
    450,
    290,
    35
  )

  walls += Wall(
    1155,
    450,
    35,
    170
  )

  walls += Wall(
    900,
    585,
    290,
    35
  )

  // Small barriers
  walls += Wall(
    610,
    120,
    120,
    28
  )

  walls += Wall(
    710,
    260,
    28,
    130
  )

  walls += Wall(
    810,
    410,
    28,
    150
  )

  walls += Wall(
    650,
    590,
    150,
    28
  )

  walls += Wall(
    580,
    330,
    110,
    28
  )

  // Loot
  loot += Loot(
    175,
    145,
    "AMMO",
    30,
    false
  )

  loot += Loot(
    515,
    125,
    "MEDKIT",
    1,
    false
  )

  loot += Loot(
    770,
    135,
    "ARMOR",
    35,
    false
  )

  loot += Loot(
    1240,
    150,
    "AMMO",
    45,
    false
  )

  loot += Loot(
    1210,
    370,
    "MEDKIT",
    1,
    false
  )

  loot += Loot(
    190,
    390,
    "ARMOR",
    30,
    false
  )

  loot += Loot(
    620,
    390,
    "AMMO",
    35,
    false
  )

  loot += Loot(
    820,
    600,
    "MEDKIT",
    1,
    false
  )

  loot += Loot(
    1240,
    640,
    "AMMO",
    40,
    false
  )

  loot += Loot(
    180,
    640,
    "MEDKIT",
    1,
    false
  )

  loot += Loot(
    700,
    680,
    "ARMOR",
    40,
    false
  )

  loot += Loot(
    1000,
    380,
    "AMMO",
    35,
    false
  )

  loot += Loot(
    870,
    210,
    "MEDKIT",
    1,
    false
  )

  spawnEnemies()
}

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

  !blocked(
    x,
    y,
    22.0
  ) &&
  dist(
    x,
    y,
    playerX,
    playerY
  ) > 220.0
}

def spawnEnemies(): Unit = {

  var created = 0
  var tries = 0

  while (
    created < 18 &&
    tries < 4000
  ) {

    tries += 1

    val x =
      randomDouble(
        140.0,
        1300.0
      )

    val y =
      randomDouble(
        130.0,
        WORLD_H - 145.0
      )

    if (
      validSpawn(
        x,
        y
      )
    ) {

      val kind =
        if (
          created % 5 ==
          0
        )
          "BRUTE"
        else if (
          created % 3 ==
          0
        )
          "STALKER"
        else
          "HUNTER"

      val multiplier =
        if (
          kind == "BRUTE"
        )
          1.55
        else if (
          kind == "STALKER"
        )
          0.82
        else
          1.0

      val hp =
        70.0 *
        multiplier *
        difficultyEnemyHealth

      val speed =
        1.3 *
        multiplier *
        difficultyEnemySpeed

      enemies += Enemy(
        x,
        y,
        randomDouble(
          -Math.PI,
          Math.PI
        ),
        hp,
        hp,
        speed,
        randomInt(
          20,
          100
        ),
        kind,
        "HUNT",
        0.0
      )

      created += 1
    }
  }

  aliveEnemies =
    enemies.length
}

buildWorld()

// ================================================================
// RESET GAME
// ================================================================

def resetGame(): Unit = {

  playerX =
    WORLD_W / 2.0

  playerY =
    WORLD_H / 2.0

  playerAngle =
    0.0

  playerHP =
    100.0

  playerArmor =
    25.0

  stamina =
    100.0

  ammo =
    MAX_AMMO

  reserveAmmo =
    120

  medkits =
    2

  kills =
    0

  survivalTime =
    0.0

  zoneRadius =
    ZONE_START

  zoneCenterX =
    WORLD_W / 2.0

  zoneCenterY =
    WORLD_H / 2.0

  paused =
    false

  running =
    true

  reloadTicks =
    0

  reloadMax =
    0

  flashTicks =
    0

  damageFlash =
    0

  screenShake =
    0.0

  animTick =
    0L

  lastShotTick =
    -100L

  lastDamageTick =
    -100L

  showHelp =
    false

  showMap =
    true

  mousePressedNow =
    false

  setDifficulty(
    difficulty
  )

  buildWorld()

  say(
    "THE NIGHT HAS BEGUN. SURVIVE.",
    260
  )
}

def startGame(): Unit = {

  resetGame()

  gameState =
    "PLAYING"

  frame.requestFocus()
}

// ================================================================
// PLAYER
// ================================================================

def updatePlayer(): Unit = {

  if (
    paused
  ) {
    return
  }

  var mx = 0.0
  var my = 0.0

  if (
    keyDown(
      KeyEvent.VK_W
    )
  ) {
    my -= 1.0
  }

  if (
    keyDown(
      KeyEvent.VK_S
    )
  ) {
    my += 1.0
  }

  if (
    keyDown(
      KeyEvent.VK_A
    )
  ) {
    mx -= 1.0
  }

  if (
    keyDown(
      KeyEvent.VK_D
    )
  ) {
    mx += 1.0
  }

  val moving =
    mx != 0.0 ||
    my != 0.0

  val sprinting =
    keyDown(
      KeyEvent.VK_SHIFT
    ) &&
    stamina > 2.0 &&
    moving

  var speed =
    PLAYER_SPEED

  if (
    sprinting
  ) {

    speed =
      SPRINT_SPEED

    stamina =
      clamp(
        stamina -
        0.95,
        0.0,
        100.0
      )
  }
  else {

    stamina =
      clamp(
        stamina +
        0.42,
        0.0,
        100.0
      )
  }

  if (
    moving
  ) {

    val len =
      Math.sqrt(
        mx * mx +
        my * my
      )

    mx /=
      len

    my /=
      len

    val pos =
      moveObject(
        playerX,
        playerY,
        playerX +
          mx *
          speed,
        playerY +
          my *
          speed,
        PLAYER_RADIUS
      )

    playerX =
      pos._1

    playerY =
      pos._2
  }

  playerAngle =
    Math.atan2(
      mouseY -
        playerY,
      mouseX -
        playerX
    )

  if (
    mousePressedNow &&
    reloadTicks <= 0
  ) {
    shoot()
  }

  if (
    keyPressedOnce(
      KeyEvent.VK_R
    )
  ) {
    startReload()
  }

  if (
    keyPressedOnce(
      KeyEvent.VK_Q
    )
  ) {
    useMedkit()
  }

  if (
    keyPressedOnce(
      KeyEvent.VK_E
    )
  ) {
    pickupNearby()
  }
}

// ================================================================
// KEYBOARD STATE
// ================================================================

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

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

def keyDown(
  code: Int
): Boolean = {

  if (
    code >= 0 &&
    code < keyStates.length
  ) {
    keyStates(code)
  }
  else {
    false
  }
}

def keyPressedOnce(
  code: Int
): Boolean = {

  if (
    code >= 0 &&
    code < keyPressedStates.length
  ) {

    if (
      keyPressedStates(code)
    ) {

      keyPressedStates(code) =
        false

      true
    }
    else {
      false
    }
  }
  else {
    false
  }
}

// ================================================================
// SHOOTING
// ================================================================

def shoot(): Unit = {

  if (
    reloadTicks > 0
  ) {
    return
  }

  if (
    ammo <= 0
  ) {

    say(
      "NO AMMO - PRESS R TO RELOAD",
      80
    )

    return
  }

  if (
    animTick -
    lastShotTick <
    7L
  ) {
    return
  }

  lastShotTick =
    animTick

  ammo -=
    1

  val spread =
    randomDouble(
      -0.035,
      0.035
    )

  val a =
    playerAngle +
    spread

  val dx =
    Math.cos(a)

  val dy =
    Math.sin(a)

  bullets += Bullet(
    playerX +
      dx *
      26.0,
    playerY +
      dy *
      26.0,
    dx *
      BULLET_SPEED,
    dy *
      BULLET_SPEED,
    90,
    true,
    28.0
  )

  flashTicks =
    3

  screenShake =
    clamp(
      screenShake +
        1.1,
      0.0,
      8.0
    )
}

def startReload(): Unit = {

  if (
    reloadTicks > 0
  ) {
    return
  }

  if (
    ammo >= MAX_AMMO
  ) {
    return
  }

  if (
    reserveAmmo <= 0
  ) {

    say(
      "NO RESERVE AMMO",
      80
    )

    return
  }

  reloadMax =
    65

  reloadTicks =
    reloadMax

  say(
    "RELOADING...",
    70
  )
}

def finishReload(): Unit = {

  val need =
    MAX_AMMO -
    ammo

  val give =
    Math.min(
      need,
      reserveAmmo
    )

  ammo +=
    give

  reserveAmmo -=
    give

  say(
    "MAGAZINE READY",
    65
  )
}

def useMedkit(): Unit = {

  if (
    medkits <= 0
  ) {

    say(
      "NO MEDKIT",
      70
    )

    return
  }

  if (
    playerHP >=
    99.0
  ) {

    say(
      "HEALTH ALREADY FULL",
      70
    )

    return
  }

  medkits -=
    1

  playerHP =
    clamp(
      playerHP +
        45.0,
      0.0,
      MAX_HP
    )

  say(
    "MEDKIT USED +45 HP",
    90
  )
}

// ================================================================
// LOOT
// ================================================================

def pickupNearby(): Unit = {

  var bestIndex =
    -1

  var bestDistance =
    55.0

  var i =
    0

  while (
    i < loot.length
  ) {

    if (
      !loot(i).taken
    ) {

      val d =
        dist(
          playerX,
          playerY,
          loot(i).x,
          loot(i).y
        )

      if (
        d < bestDistance
      ) {

        bestDistance =
          d

        bestIndex =
          i
      }
    }

    i +=
      1
  }

  if (
    bestIndex >= 0
  ) {

    val item =
      loot(bestIndex)

    item.taken =
      true

    if (
      item.kind ==
      "AMMO"
    ) {

      reserveAmmo =
        Math.min(
          240,
          reserveAmmo +
            item.amount
        )

      say(
        "AMMO +" +
        item.amount,
        80
      )
    }
    else if (
      item.kind ==
      "MEDKIT"
    ) {

      medkits =
        Math.min(
          5,
          medkits +
            item.amount
        )

      say(
        "MEDKIT +1",
        80
      )
    }
    else if (
      item.kind ==
      "ARMOR"
    ) {

      playerArmor =
        clamp(
          playerArmor +
            item.amount.toDouble,
          0.0,
          MAX_ARMOR
        )

      say(
        "ARMOR +" +
        item.amount,
        80
      )
    }
  }
  else {

    say(
      "NO LOOT NEARBY",
      70
    )
  }
}

// ================================================================
// BULLET UPDATE
// ================================================================

def updateBullets(): Unit = {

  var i =
    bullets.length -
    1

  while (
    i >= 0
  ) {

    val b =
      bullets(i)

    b.x +=
      b.dx

    b.y +=
      b.dy

    b.life -=
      1

    var remove =
      b.life <= 0

    if (
      !remove &&
      blocked(
        b.x,
        b.y,
        3.0
      )
    ) {

      remove =
        true
    }

    if (
      !remove &&
      b.fromPlayer
    ) {

      var j =
        enemies.length -
        1

      while (
        j >= 0 &&
        !remove
      ) {

        val e =
          enemies(j)

        if (
          dist(
            b.x,
            b.y,
            e.x,
            e.y
          ) <
          22.0
        ) {

          e.hp -=
            b.damage

          e.fear =
            clamp(
              e.fear +
                25.0,
              0.0,
              100.0
            )

          remove =
            true

          screenShake =
            clamp(
              screenShake +
                0.4,
              0.0,
              8.0
            )

          if (
            e.hp <=
            0.0
          ) {

            kills +=
              1

            if (
              kills %
              4 ==
              0
            ) {

              say(
                "HUNTER DOWN",
                80
              )
            }

            maybeDropLoot(
              e.x,
              e.y
            )

            enemies.remove(j)
            aliveEnemies =
              enemies.length
          }
        }

        j -=
          1
      }
    }
    else if (
      !remove &&
      !b.fromPlayer
    ) {

      if (
        dist(
          b.x,
          b.y,
          playerX,
          playerY
        ) <
        PLAYER_RADIUS +
        4.0
      ) {

        damagePlayer(
          b.damage
        )

        remove =
          true
      }
    }

    if (
      remove
    ) {
      bullets.remove(i)
    }

    i -=
      1
  }
}

def maybeDropLoot(
  x: Double,
  y: Double
): Unit = {

  val chance =
    random.nextDouble()

  if (
    chance <
    0.42
  ) {

    loot += Loot(
      x,
      y,
      "AMMO",
      randomInt(
        10,
        26
      ),
      false
    )
  }
  else if (
    chance <
    0.63
  ) {

    loot += Loot(
      x,
      y,
      "ARMOR",
      randomInt(
        12,
        28
      ),
      false
    )
  }
  else if (
    chance <
    0.78
  ) {

    loot += Loot(
      x,
      y,
      "MEDKIT",
      1,
      false
    )
  }
}

// ================================================================
// PLAYER DAMAGE
// ================================================================

def damagePlayer(
  damage: Double
): Unit = {

  if (
    animTick -
    lastDamageTick <
    4L
  ) {
    return
  }

  lastDamageTick =
    animTick

  var remaining =
    damage *
    difficultyEnemyDamage

  if (
    playerArmor >
    0.0
  ) {

    val armorDamage =
      Math.min(
        playerArmor,
        remaining *
          0.60
      )

    playerArmor -=
      armorDamage

    remaining -=
      armorDamage
  }

  playerHP -=
    remaining

  playerHP =
    clamp(
      playerHP,
      0.0,
      MAX_HP
    )

  damageFlash =
    14

  screenShake =
    clamp(
      screenShake +
        3.0,
      0.0,
      10.0
    )

  if (
    playerHP <=
    0.0
  ) {

    gameState =
      "LOSE"

    running =
      false
  }
}

// ================================================================
// ENEMY AI
// ================================================================

def updateEnemies(): Unit = {

  var i =
    enemies.length -
    1

  while (
    i >= 0
  ) {

    val e =
      enemies(i)

    val d =
      dist(
        e.x,
        e.y,
        playerX,
        playerY
      )

    val targetAngle =
      Math.atan2(
        playerY -
          e.y,
        playerX -
          e.x
      )

    e.angle =
      normAngle(
        e.angle +
          clamp(
            normAngle(
              targetAngle -
                e.angle
            ),
            -0.065,
            0.065
          )
      )

    if (
      d < 520.0
    ) {

      e.state =
        "CHASE"
    }
    else {

      e.state =
        "ROAM"
    }

    if (
      e.fear >
      0.0
    ) {

      e.fear =
        clamp(
          e.fear -
            0.22,
          0.0,
          100.0
        )
    }

    var moveSpeed =
      e.speed

    if (
      e.kind ==
      "STALKER"
    ) {

      moveSpeed *=
        1.18

      if (
        d < 220.0
      ) {
        moveSpeed *=
          1.12
      }
    }

    if (
      e.kind ==
      "BRUTE"
    ) {

      moveSpeed *=
        0.72
    }

    if (
      d > 48.0 &&
      d < 700.0
    ) {

      val nx =
        e.x +
          Math.cos(
            e.angle
          ) *
          moveSpeed

      val ny =
        e.y +
          Math.sin(
            e.angle
          ) *
          moveSpeed

      val pos =
        moveObject(
          e.x,
          e.y,
          nx,
          ny,
          20.0
        )

      e.x =
        pos._1

      e.y =
        pos._2
    }

    if (
      d <
      300.0 &&
      !lineBlocked(
        e.x,
        e.y,
        playerX,
        playerY
      )
    ) {

      if (
        e.cooldown >
        0
      ) {

        e.cooldown -=
          1
      }
      else {

        enemyShoot(
          e
        )

        e.cooldown =
          if (
            e.kind ==
            "STALKER"
          )
            48
          else if (
            e.kind ==
            "BRUTE"
          )
            90
          else
            68
      }
    }
    else {

      if (
        e.cooldown >
        0
      ) {
        e.cooldown -=
          1
      }
    }

    if (
      d <
      (
        if (
          e.kind ==
          "BRUTE"
        )
          32.0
        else
          27.0
      )
    ) {

      if (
        e.kind ==
        "BRUTE"
      ) {

        damagePlayer(
          7.5
        )
      }
      else {

        damagePlayer(
          4.0
        )
      }
    }

    i -=
      1
  }

  aliveEnemies =
    enemies.length
}

def enemyShoot(
  e: Enemy
): Unit = {

  val baseAngle =
    Math.atan2(
      playerY -
        e.y,
      playerX -
        e.x
    )

  val spread =
    if (
      e.kind ==
      "BRUTE"
    )
      0.18
    else if (
      e.kind ==
      "STALKER"
    )
      0.05
    else
      0.10

  val a =
    baseAngle +
      randomDouble(
        -spread,
        spread
      )

  val damage =
    if (
      e.kind ==
      "BRUTE"
    )
      20.0
    else if (
      e.kind ==
      "STALKER"
    )
      11.0
    else
      14.0

  bullets += Bullet(
    e.x +
      Math.cos(a) *
      23.0,
    e.y +
      Math.sin(a) *
      23.0,
    Math.cos(a) *
      (
        BULLET_SPEED *
        0.76
      ),
    Math.sin(a) *
      (
        BULLET_SPEED *
        0.76
      ),
    100,
    false,
    damage
  )
}

// ================================================================
// ZONE
// ================================================================

def updateZone(): Unit = {

  val shrinkRate =
    if (
      survivalTime <
      30.0
    )
      0.0
    else if (
      survivalTime <
      80.0
    )
      0.38
    else if (
      survivalTime <
      140.0
    )
      0.72
    else
      1.0

  if (
    zoneRadius >
    ZONE_MIN
  ) {

    zoneRadius =
      Math.max(
        ZONE_MIN,
        zoneRadius -
          shrinkRate *
          0.016
      )
  }

  val d =
    dist(
      playerX,
      playerY,
      zoneCenterX,
      zoneCenterY
    )

  if (
    d >
    zoneRadius
  ) {

    damagePlayer(
      3.2 *
      difficultyZoneDamage
    )

    if (
      animTick %
      60L ==
      0L
    ) {

      say(
        "THE STORM IS CLOSING IN!",
        75
      )
    }
  }
}

// ================================================================
// WIN CONDITION
// ================================================================

def checkWin(): Unit = {

  if (
    gameState !=
    "PLAYING"
  ) {
    return
  }

  if (
    enemies.isEmpty
  ) {

    gameState =
      "WIN"

    running =
      false

    say(
      "THE NIGHT IS YOURS.",
      300
    )
  }
}

// ================================================================
// TIMER UPDATE
// ================================================================

def updateGame(): Unit = {

  if (
    gameState !=
    "PLAYING" ||
    paused ||
    !running
  ) {
    return
  }

  survivalTime +=
    0.016

  animTick +=
    1L

  if (
    messageTicks >
    0
  ) {

    messageTicks -=
      1
  }

  if (
    flashTicks >
    0
  ) {

    flashTicks -=
      1
  }

  if (
    damageFlash >
    0
  ) {

    damageFlash -=
      1
  }

  if (
    screenShake >
    0.0
  ) {

    screenShake =
      Math.max(
        0.0,
        screenShake -
          0.25
      )
  }

  if (
    reloadTicks >
    0
  ) {

    reloadTicks -=
      1

    if (
      reloadTicks ==
      0
    ) {

      finishReload()
    }
  }

  updatePlayer()
  updateBullets()
  updateEnemies()
  updateZone()
  checkWin()
}

// ================================================================
// DRAW WORLD
// ================================================================

def drawBackground(
  g: Graphics2D
): Unit = {

  g.setColor(
    new Color(
      7,
      9,
      14
    )
  )

  g.fillRect(
    0,
    0,
    WORLD_W,
    WORLD_H
  )

  // Ground grid
  g.setColor(
    new Color(
      20,
      23,
      31
    )
  )

  var gx =
    0

  while (
    gx <= WORLD_W
  ) {

    g.drawLine(
      gx,
      0,
      gx,
      WORLD_H
    )

    gx +=
      80
  }

  var gy =
    0

  while (
    gy <= WORLD_H
  ) {

    g.drawLine(
      0,
      gy,
      WORLD_W,
      gy
    )

    gy +=
      80
  }

  // Ground patches
  var i =
    0

  while (
    i < 80
  ) {

    val x =
      (
        i * 173
      ) %
      WORLD_W

    val y =
      (
        i * 97
      ) %
      WORLD_H

    val r =
      2 +
      (
        i %
        5
      )

    g.setColor(
      new Color(
        28,
        30,
        36,
        100
      )
    )

    g.fillOval(
      x,
      y,
      r,
      r
    )

    i +=
      1
  }
}

def drawWalls(
  g: Graphics2D
): Unit = {

  var i =
    0

  while (
    i < walls.length
  ) {

    val w =
      walls(i)

    g.setColor(
      new Color(
        25,
        28,
        35
      )
    )

    g.fillRect(
      w.x,
      w.y,
      w.w,
      w.h
    )

    g.setColor(
      new Color(
        78,
        82,
        92
      )
    )

    g.drawRect(
      w.x,
      w.y,
      w.w,
      w.h
    )

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

    g.drawLine(
      w.x,
      w.y,
      w.x +
        w.w,
      w.y +
        w.h
    )

    i +=
      1
  }
}

// ================================================================
// DRAW LOOT
// ================================================================

def drawLoot(
  g: Graphics2D
): Unit = {

  var i =
    0

  while (
    i < loot.length
  ) {

    val item =
      loot(i)

    if (
      !item.taken
    ) {

      val pulse =
        Math.sin(
          animTick.toDouble *
            0.08 +
            i
        ) *
        3.0

      if (
        item.kind ==
        "AMMO"
      ) {

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

        g.fillRoundRect(
          (
            item.x -
              10.0
          ).toInt,
          (
            item.y -
              7.0 +
              pulse
          ).toInt,
          20,
          14,
          5,
          5
        )
      }
      else if (
        item.kind ==
        "MEDKIT"
      ) {

        g.setColor(
          new Color(
            70,
            210,
            125
          )
        )

        g.fillRoundRect(
          (
            item.x -
              12.0
          ).toInt,
          (
            item.y -
              10.0 +
              pulse
          ).toInt,
          24,
          20,
          5,
          5
        )

        g.setColor(
          Color.WHITE
        )

        g.fillRect(
          (
            item.x -
              2.0
          ).toInt,
          (
            item.y -
              7.0 +
              pulse
          ).toInt,
          4,
          14
        )

        g.fillRect(
          (
            item.x -
              7.0
          ).toInt,
          (
            item.y -
              2.0 +
              pulse
          ).toInt,
          14,
          4
        )
      }
      else {

        g.setColor(
          new Color(
            90,
            145,
            235
          )
        )

        g.fillOval(
          (
            item.x -
              12.0
          ).toInt,
          (
            item.y -
              12.0 +
              pulse
          ).toInt,
          24,
          24
        )

        g.setColor(
          new Color(
            190,
            220,
            255
          )
        )

        g.drawOval(
          (
            item.x -
              15.0
          ).toInt,
          (
            item.y -
              15.0 +
              pulse
          ).toInt,
          30,
          30
        )
      }
    }

    i +=
      1
  }
}

// ================================================================
// DRAW ENEMIES
// ================================================================

def drawEnemies(
  g: Graphics2D
): Unit = {

  var i =
    0

  while (
    i < enemies.length
  ) {

    val e =
      enemies(i)

    val glow =
      if (
        e.kind ==
        "BRUTE"
      )
        22
      else if (
        e.kind ==
        "STALKER"
      )
        16
      else
        12

    g.setColor(
      new Color(
        120,
        10,
        22,
        30
      )
    )

    g.fillOval(
      (
        e.x -
          30.0
      ).toInt,
      (
        e.y -
          30.0
      ).toInt,
      60,
      60
    )

    if (
      e.kind ==
      "BRUTE"
    ) {

      g.setColor(
        new Color(
          70,
          55,
          62
        )
      )

      g.fillOval(
        (
          e.x -
            24.0
        ).toInt,
        (
          e.y -
            26.0
        ).toInt,
        48,
        52
      )

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

      g.fillOval(
        (
          e.x -
            12.0
        ).toInt,
        (
          e.y -
            8.0
        ).toInt,
        7,
        7
      )

      g.fillOval(
        (
          e.x +
            5.0
        ).toInt,
        (
          e.y -
            8.0
        ).toInt,
        7,
        7
      )
    }
    else if (
      e.kind ==
      "STALKER"
    ) {

      g.setColor(
        new Color(
          24,
          28,
          38
        )
      )

      g.fillOval(
        (
          e.x -
            17.0
        ).toInt,
        (
          e.y -
            21.0
        ).toInt,
        34,
        44
      )

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

      g.fillOval(
        (
          e.x -
            8.0
        ).toInt,
        (
          e.y -
            7.0
        ).toInt,
        6,
        6
      )

      g.fillOval(
        (
          e.x +
            3.0
        ).toInt,
        (
          e.y -
            7.0
        ).toInt,
        6,
        6
      )
    }
    else {

      g.setColor(
        new Color(
          42,
          44,
          53
        )
      )

      g.fillOval(
        (
          e.x -
            19.0
        ).toInt,
        (
          e.y -
            22.0
        ).toInt,
        38,
        42
      )

      g.setColor(
        new Color(
          220,
          55,
          70
        )
      )

      g.fillOval(
        (
          e.x -
            8.0
        ).toInt,
        (
          e.y -
            6.0
        ).toInt,
        6,
        6
      )

      g.fillOval(
        (
          e.x +
            3.0
        ).toInt,
        (
          e.y -
            6.0
        ).toInt,
        6,
        6
      )
    }

    // Health bar
    val barW =
      if (
        e.kind ==
        "BRUTE"
      )
        52
      else
        42

    val hpRatio =
      clamp(
        e.hp /
          e.maxHp,
        0.0,
        1.0
      )

    g.setColor(
      new Color(
        8,
        8,
        10,
        200
      )
    )

    g.fillRect(
      (
        e.x -
          barW /
          2.0
      ).toInt,
      (
        e.y -
          36.0
      ).toInt,
      barW,
      5
    )

    g.setColor(
      new Color(
        215,
        60,
        72
      )
    )

    g.fillRect(
      (
        e.x -
          barW /
          2.0
      ).toInt,
      (
        e.y -
          36.0
      ).toInt,
      (
        barW *
          hpRatio
      ).toInt,
      5
    )

    i +=
      1
  }
}

// ================================================================
// DRAW BULLETS
// ================================================================

def drawBullets(
  g: Graphics2D
): Unit = {

  var i =
    0

  while (
    i < bullets.length
  ) {

    val b =
      bullets(i)

    if (
      b.fromPlayer
    ) {

      g.setColor(
        new Color(
          255,
          238,
          170
        )
      )
    }
    else {

      g.setColor(
        new Color(
          255,
          65,
          85
        )
      )
    }

    g.fillOval(
      (
        b.x -
          3.0
      ).toInt,
      (
        b.y -
          3.0
      ).toInt,
      6,
      6
    )

    i +=
      1
  }
}

// ================================================================
// PLAYER DRAW
// ================================================================

def drawPlayer(
  g: Graphics2D
): Unit = {

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

  g.fillOval(
    (
      playerX -
        24.0
    ).toInt,
    (
      playerY -
        19.0
    ).toInt,
    48,
    38
  )

  g.setColor(
    new Color(
      60,
      170,
      235
    )
  )

  g.fillOval(
    (
      playerX -
        17.0
    ).toInt,
    (
      playerY -
        17.0
    ).toInt,
    34,
    34
  )

  g.setColor(
    new Color(
      180,
      220,
      255
    )
  )

  g.drawOval(
    (
      playerX -
        21.0
    ).toInt,
    (
      playerY -
        21.0
    ).toInt,
    42,
    42
  )

  val gunX =
    playerX +
      Math.cos(
        playerAngle
      ) *
      27.0

  val gunY =
    playerY +
      Math.sin(
        playerAngle
      ) *
      27.0

  g.setStroke(
    new BasicStroke(
      6.0f
    )
  )

  g.setColor(
    new Color(
      30,
      34,
      40
    )
  )

  g.drawLine(
    playerX.toInt,
    playerY.toInt,
    gunX.toInt,
    gunY.toInt
  )

  g.setStroke(
    new BasicStroke(
      1.0f
    )
  )
}

// ================================================================
// LIGHTING / FOG
// ================================================================

def drawLighting(
  g: Graphics2D
): Unit = {

  val darkness =
    if (
      difficulty ==
      "HARD"
    )
      132
    else
      112

  g.setColor(
    new Color(
      0,
      0,
      8,
      darkness
    )
  )

  g.fillRect(
    0,
    0,
    WORLD_W,
    WORLD_H
  )

  val r1 =
    165 +
      (
        Math.sin(
          animTick.toDouble *
            0.06
        ) *
        9.0
      ).toInt

  g.setColor(
    new Color(
      255,
      242,
      205,
      28
    )
  )

  g.fillOval(
    (
      playerX -
        r1
    ).toInt,
    (
      playerY -
        r1
    ).toInt,
    r1 * 2,
    r1 * 2
  )

  val r2 =
    90

  g.setColor(
    new Color(
      255,
      245,
      220,
      25
    )
  )

  g.fillOval(
    (
      playerX -
        r2
    ).toInt,
    (
      playerY -
        r2
    ).toInt,
    r2 * 2,
    r2 * 2
  )

  // Zone overlay
  g.setColor(
    new Color(
      90,
      120,
      170,
      40
    )
  )

  g.fillOval(
    (
      zoneCenterX -
        zoneRadius
    ).toInt,
    (
      zoneCenterY -
        zoneRadius
    ).toInt,
    (
      zoneRadius *
        2.0
    ).toInt,
    (
      zoneRadius *
        2.0
    ).toInt
  )

  g.setColor(
    new Color(
      90,
      180,
      235,
      170
    )
  )

  g.setStroke(
    new BasicStroke(
      4.0f
    )
  )

  g.drawOval(
    (
      zoneCenterX -
        zoneRadius
    ).toInt,
    (
      zoneCenterY -
        zoneRadius
    ).toInt,
    (
      zoneRadius *
        2.0
    ).toInt,
    (
      zoneRadius *
        2.0
    ).toInt
  )

  g.setStroke(
    new BasicStroke(
      1.0f
    )
  )

  // Border storm effect
  g.setColor(
    new Color(
      35,
      80,
      145,
      32
    )
  )

  g.fillRect(
    0,
    0,
    40,
    WORLD_H
  )

  g.fillRect(
    WORLD_W -
      40,
    0,
    40,
    WORLD_H
  )

  g.fillRect(
    0,
    0,
    WORLD_W,
    40
  )

  g.fillRect(
    0,
    WORLD_H -
      40,
    WORLD_W,
    40
  )
}

// ================================================================
// MINIMAP
// ================================================================

def drawMiniMap(
  g: Graphics2D
): Unit = {

  if (
    !showMap
  ) {
    return
  }

  val x0 =
    20

  val y0 =
    18

  val mw =
    225

  val mh =
    155

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

  g.fillRoundRect(
    x0,
    y0,
    mw,
    mh,
    16,
    16
  )

  val scaleX =
    mw.toDouble /
      WORLD_W.toDouble

  val scaleY =
    mh.toDouble /
      WORLD_H.toDouble

  var i =
    0

  while (
    i < walls.length
  ) {

    val w =
      walls(i)

    g.setColor(
      new Color(
        70,
        75,
        85
      )
    )

    g.fillRect(
      (
        x0 +
          w.x *
          scaleX
      ).toInt,
      (
        y0 +
          w.y *
          scaleY
      ).toInt,
      Math.max(
        1,
        (
          w.w *
            scaleX
        ).toInt
      ),
      Math.max(
        1,
        (
          w.h *
            scaleY
        ).toInt
      )
    )

    i +=
      1
  }

  g.setColor(
    new Color(
      65,
      180,
      240
    )
  )

  g.fillOval(
    (
      x0 +
        playerX *
        scaleX -
        4.0
    ).toInt,
    (
      y0 +
        playerY *
        scaleY -
        4.0
    ).toInt,
    8,
    8
  )

  var eIndex =
    0

  while (
    eIndex <
    enemies.length
  ) {

    val e =
      enemies(eIndex)

    g.setColor(
      new Color(
        225,
        60,
        70
      )
    )

    g.fillOval(
      (
        x0 +
          e.x *
          scaleX -
          3.0
      ).toInt,
      (
        y0 +
          e.y *
          scaleY -
          3.0
      ).toInt,
      6,
      6
    )

    eIndex +=
      1
  }

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

  g.drawOval(
    (
      x0 +
        (
          zoneCenterX -
            zoneRadius
        ) *
        scaleX
    ).toInt,
    (
      y0 +
        (
          zoneCenterY -
            zoneRadius
        ) *
        scaleY
    ).toInt,
    (
      zoneRadius *
        2.0 *
        scaleX
    ).toInt,
    (
      zoneRadius *
        2.0 *
        scaleY
    ).toInt
  )

  g.setColor(
    Color.WHITE
  )

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

  g.drawString(
    "NIGHT MAP",
    x0 +
      10,
    y0 +
      16
  )
}

// ================================================================
// HUD
// ================================================================

def drawHud(
  g: Graphics2D
): Unit = {

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

  g.fillRect(
    0,
    WORLD_H,
    GAME_W,
    HUD_H
  )

  // HP
  drawStatusBar(
    g,
    20,
    WORLD_H +
      16,
    230,
    playerHP,
    100.0,
    "HEALTH",
    new Color(
      220,
      70,
      85
    )
  )

  // Armor
  drawStatusBar(
    g,
    270,
    WORLD_H +
      16,
    230,
    playerArmor,
    100.0,
    "ARMOR",
    new Color(
      80,
      150,
      235
    )
  )

  // Stamina
  drawStatusBar(
    g,
    520,
    WORLD_H +
      16,
    190,
    stamina,
    100.0,
    "STAMINA",
    new Color(
      80,
      205,
      130
    )
  )

  g.setColor(
    Color.WHITE
  )

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

  g.drawString(
    "AMMO " +
      ammo +
      "/" +
      reserveAmmo,
    735,
    WORLD_H +
      31
  )

  g.drawString(
    "MEDKITS " +
      medkits,
    865,
    WORLD_H +
      31
  )

  g.drawString(
    "KILLS " +
      kills,
    1005,
    WORLD_H +
      31
  )

  g.drawString(
    "ENEMIES " +
      aliveEnemies,
    1115,
    WORLD_H +
      31
  )

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

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

  g.drawString(
    "TIME " +
      formatTime(
        survivalTime
      ),
    20,
    WORLD_H +
      61
  )

  g.drawString(
    "ZONE " +
      zoneRadius.toInt,
    145,
    WORLD_H +
      61
  )

  g.drawString(
    "WASD MOVE   SHIFT SPRINT   MOUSE AIM   LMB FIRE   R RELOAD   Q MEDKIT   E LOOT   P PAUSE   M MAP",
    300,
    WORLD_H +
      61
  )

  if (
    messageTicks >
    0
  ) {

    g.setColor(
      new Color(
        255,
        233,
        170
      )
    )

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

    g.drawString(
      message,
      20,
      WORLD_H -
        12
    )
  }

  if (
    reloadTicks >
    0
  ) {

    val ratio =
      1.0 -
        reloadTicks.toDouble /
          reloadMax.toDouble

    g.setColor(
      new Color(
        12,
        14,
        18,
        230
      )
    )

    g.fillRoundRect(
      GAME_W / 2 -
        130,
      WORLD_H -
        54,
      260,
      25,
      10,
      10
    )

    g.setColor(
      new Color(
        230,
        190,
        90
      )
    )

    g.fillRoundRect(
      GAME_W / 2 -
        130,
      WORLD_H -
        54,
      (
        260 *
          ratio
      ).toInt,
      25,
      10,
      10
    )

    g.setColor(
      Color.WHITE
    )

    g.drawString(
      "RELOADING",
      GAME_W / 2 -
        42,
      WORLD_H -
        36
    )
  }
}

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

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

  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 +
      14
  )
}

def formatTime(
  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
}

// ================================================================
// CROSSHAIR
// ================================================================

def drawCrosshair(
  g: Graphics2D
): Unit = {

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

  val cx =
    mouseX

  val cy =
    mouseY

  g.drawLine(
    cx -
      10,
    cy,
    cx -
      3,
    cy
  )

  g.drawLine(
    cx +
      3,
    cy,
    cx +
      10,
    cy
  )

  g.drawLine(
    cx,
    cy -
      10,
    cx,
    cy -
      3
  )

  g.drawLine(
    cx,
    cy +
      3,
    cx,
    cy +
      10
  )

  g.drawOval(
    cx -
      2,
    cy -
      2,
    4,
    4
  )
}

// ================================================================
// HELP SCREEN
// ================================================================

def drawHelp(
  g: Graphics2D
): Unit = {

  if (
    !showHelp
  ) {
    return
  }

  g.setColor(
    new Color(
      3,
      5,
      10,
      245
    )
  )

  g.fillRoundRect(
    250,
    90,
    940,
    525,
    24,
    24
  )

  g.setColor(
    Color.WHITE
  )

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

  g.drawString(
    "NIGHTFALL : HOW TO SURVIVE",
    500,
    140
  )

  val lines =
    Array(
      "WASD        Move your character",
      "SHIFT       Sprint while stamina is available",
      "MOUSE       Aim the weapon",
      "LEFT CLICK  Fire",
      "R           Reload",
      "Q           Use a medkit",
      "E           Pick up nearby loot",
      "M           Toggle minimap",
      "P / ESC     Pause / resume",
      "",
      "AMMO   Yellow boxes",
      "MEDKIT Green boxes",
      "ARMOR  Blue orbs",
      "",
      "Stay inside the blue safe zone.",
      "Enemies become dangerous when they get close.",
      "Eliminate all hunters to clear the night.",
      "",
      "This is an original Java2D game and uses no external assets."
    )

  g.setColor(
    new Color(
      220,
      228,
      238
    )
  )

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

  var i =
    0

  while (
    i < lines.length
  ) {

    g.drawString(
      lines(i),
      310,
      180 +
        i *
        22
    )

    i +=
      1
  }

  g.setColor(
    new Color(
      245,
      215,
      120
    )
  )

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

  g.drawString(
    "Press F1 to close this screen",
    585,
    590
  )
}

// ================================================================
// MENU
// ================================================================

def drawMenu(
  g: Graphics2D
): Unit = {

  g.setColor(
    new Color(
      2,
      3,
      7
    )
  )

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

  // Moon
  g.setColor(
    new Color(
      210,
      215,
      225,
      100
    )
  )

  g.fillOval(
    1070,
    80,
    150,
    150
  )

  // Horror fog
  var i =
    0

  while (
    i < 16
  ) {

    g.setColor(
      new Color(
        60,
        80,
        105,
        14
      )
    )

    g.fillOval(
      60 +
        i *
        95,
      300 +
        (
          Math.sin(
            i
          ) *
          60
        ).toInt,
      280,
      100
    )

    i +=
      1
  }

  g.setColor(
    new Color(
      14,
      18,
      26
    )
  )

  g.fillRect(
    230,
    250,
    980,
    330
  )

  // House silhouette
  val roof =
    new Polygon()

  roof.addPoint(
    280,
    250
  )

  roof.addPoint(
    720,
    105
  )

  roof.addPoint(
    1160,
    250
  )

  g.setColor(
    new Color(
      8,
      10,
      15
    )
  )

  g.fillPolygon(
    roof
  )

  g.setColor(
    new Color(
      30,
      35,
      45
    )
  )

  g.fillRect(
    410,
    300,
    620,
    280
  )

  // Windows
  var wx =
    450

  while (
    wx < 980
  ) {

    g.setColor(
      new Color(
        220,
        180,
        80,
        160
      )
    )

    g.fillRect(
      wx,
      350,
      52,
      60
    )

    wx +=
      90
  }

  g.setColor(
    new Color(
      220,
      230,
      245
    )
  )

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

  g.drawString(
    "NIGHTFALL",
    530,
    180
  )

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

  g.setColor(
    new Color(
      230,
      75,
      90
    )
  )

  g.drawString(
    "HORROR SURVIVAL",
    590,
    220
  )

  g.setColor(
    new Color(
      190,
      200,
      215
    )
  )

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

  g.drawString(
    "SURVIVE THE STORM  ?  HUNT THE HUNTERS  ?  ESCAPE THE NIGHT",
    455,
    610
  )

  // Buttons
  drawMenuButton(
    g,
    510,
    650,
    420,
    52,
    "START GAME"
  )

  drawMenuButton(
    g,
    510,
    714,
    420,
    52,
    "HOW TO PLAY"
  )

  drawMenuButton(
    g,
    510,
    778,
    420,
    52,
    "QUIT"
  )

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

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

  g.drawString(
    "DIFFICULTY:",
    970,
    668
  )

  drawDifficultyButton(
    g,
    970,
    685,
    "EASY"
  )

  drawDifficultyButton(
    g,
    1055,
    685,
    "NORMAL"
  )

  drawDifficultyButton(
    g,
    970,
    730,
    "HARD"
  )

  if (
    showHelp
  ) {
    drawHelp(
      g
    )
  }
}

def drawMenuButton(
  g: Graphics2D,
  x: Int,
  y: Int,
  w: Int,
  h: Int,
  text: String
): Unit = {

  g.setColor(
    new Color(
      22,
      27,
      38,
      240
    )
  )

  g.fillRoundRect(
    x,
    y,
    w,
    h,
    16,
    16
  )

  g.setColor(
    new Color(
      105,
      120,
      145
    )
  )

  g.drawRoundRect(
    x,
    y,
    w,
    h,
    16,
    16
  )

  g.setColor(
    Color.WHITE
  )

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

  val tw =
    g.getFontMetrics.stringWidth(
      text
    )

  g.drawString(
    text,
    x +
      (
        w -
        tw
      ) /
        2,
    y +
      33
  )
}

def drawDifficultyButton(
  g: Graphics2D,
  x: Int,
  y: Int,
  text: String
): Unit = {

  if (
    text ==
    difficulty
  ) {

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

    g.setColor(
      new Color(
        35,
        42,
        55
      )
    )
  }

  g.fillRoundRect(
    x,
    y,
    78,
    36,
    10,
    10
  )

  g.setColor(
    Color.WHITE
  )

  g.drawRoundRect(
    x,
    y,
    78,
    36,
    10,
    10
  )

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

  g.drawString(
    text,
    x +
      10,
    y +
      23
  )
}

// ================================================================
// END SCREEN
// ================================================================

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

  if (
    won
  ) {

    g.setColor(
      new Color(
        5,
        30,
        25
      )
    )
  }
  else {

    g.setColor(
      new Color(
        28,
        5,
        12
      )
    )
  }

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

  g.setColor(
    Color.WHITE
  )

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

  g.drawString(
    if (
      won
    )
      "NIGHT SURVIVED"
    else
      "YOU WERE HUNTED",
    455,
    210
  )

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

  g.drawString(
    "SURVIVAL TIME : " +
      formatTime(
        survivalTime
      ),
    545,
    280
  )

  g.drawString(
    "HUNTERS DOWN : " +
      kills,
    570,
    325
  )

  g.drawString(
    "DIFFICULTY : " +
      difficulty,
    590,
    370
  )

  g.setColor(
    new Color(
      225,
      235,
      245
    )
  )

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

  g.drawString(
    "ENTER / R  =  RESTART",
    585,
    450
  )

  g.drawString(
    "M  =  MAIN MENU",
    610,
    485
  )
}

// ================================================================
// FULL PLAYING SCREEN
// ================================================================

def drawGame(
  g: Graphics2D
): Unit = {

  val old =
    g.getTransform

  var sx =
    0

  var sy =
    0

  if (
    screenShake >
    0.0
  ) {

    sx =
      randomInt(
        -screenShake.toInt,
        screenShake.toInt
      )

    sy =
      randomInt(
        -screenShake.toInt,
        screenShake.toInt
      )
  }

  g.translate(
    sx,
    sy
  )

  drawBackground(g)
  drawWalls(g)
  drawLoot(g)
  drawEnemies(g)
  drawBullets(g)
  drawPlayer(g)
  drawLighting(g)
  drawMiniMap(g)
  drawCrosshair(g)

  g.setTransform(
    old
  )

  drawHud(g)

  if (
    paused
  ) {

    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",
      620,
      300
    )

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

    g.drawString(
      "Press P or ESC to resume",
      575,
      345
    )
  }

  if (
    damageFlash >
    0
  ) {

    g.setColor(
      new Color(
        210,
        40,
        55,
        35 +
          damageFlash *
          5
      )
    )

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

  if (
    flashTicks >
    0
  ) {

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

    val fx =
      playerX.toInt

    val fy =
      playerY.toInt

    g.fillOval(
      fx -
        50,
      fy -
        50,
      100,
      100
    )
  }

  if (
    showHelp
  ) {

    drawHelp(
      g
    )
  }
}

// ================================================================
// PANEL
// ================================================================

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 (
        gameState ==
        "MENU"
      ) {

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

        drawGame(
          g
        )
      }
      else if (
        gameState ==
        "WIN"
      ) {

        drawEndScreen(
          g,
          true
        )
      }
      else if (
        gameState ==
        "LOSE"
      ) {

        drawEndScreen(
          g,
          false
        )
      }
    }

    addKeyListener(
      new KeyAdapter {

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

          val c =
            e.getKeyCode

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

            if (
              !keyStates(c)
            ) {

              keyPressedStates(c) =
                true
            }

            keyStates(c) =
              true
          }

          if (
            c ==
            KeyEvent.VK_ESCAPE
          ) {

            if (
              gameState ==
              "PLAYING"
            ) {

              paused =
                !paused
            }
          }

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

            paused =
              !paused
          }

          if (
            c ==
            KeyEvent.VK_F1
          ) {

            showHelp =
              !showHelp
          }

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

            showMap =
              !showMap
          }

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

            startGame()
          }

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

            gameState =
              "MENU"

            running =
              true
          }

          repaint()
        }

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

          val c =
            e.getKeyCode

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

            keyStates(c) =
              false
          }
        }
      }
    )

    addMouseListener(
      new MouseAdapter {

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

          mouseX =
            e.getX

          mouseY =
            e.getY

          if (
            gameState ==
            "MENU"
          ) {

            handleMenuClick(
              mouseX,
              mouseY
            )
          }
          else if (
            gameState ==
            "PLAYING"
          ) {

            mousePressedNow =
              true
          }

          requestFocusInWindow()

          repaint()
        }

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

          mousePressedNow =
            false

          repaint()
        }
      }
    )

    addMouseMotionListener(
      new MouseMotionAdapter {

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

          mouseX =
            e.getX

          mouseY =
            e.getY

          repaint()
        }

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

          mouseX =
            e.getX

          mouseY =
            e.getY

          repaint()
        }
      }
    )
  }

// ================================================================
// MENU CLICK
// ================================================================

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

  if (
    x >= 510 &&
    x <= 930 &&
    y >= 650 &&
    y < 702
  ) {

    startGame()
  }
  else if (
    x >= 510 &&
    x <= 930 &&
    y >= 714 &&
    y < 766
  ) {

    showHelp =
      !showHelp
  }
  else if (
    x >= 510 &&
    x <= 930 &&
    y >= 778 &&
    y < 830
  ) {

    frame.dispose()
  }

  if (
    x >= 970 &&
    x < 1048 &&
    y >= 685 &&
    y < 721
  ) {

    setDifficulty(
      "EASY"
    )
  }

  if (
    x >= 1055 &&
    x < 1133 &&
    y >= 685 &&
    y < 721
  ) {

    setDifficulty(
      "NORMAL"
    )
  }

  if (
    x >= 970 &&
    x < 1048 &&
    y >= 730 &&
    y < 766
  ) {

    setDifficulty(
      "HARD"
    )
  }
}

// ================================================================
// INITIAL FOCUS
// ================================================================

frame.setContentPane(
  panel
)

frame.setVisible(
  true
)

panel.requestFocusInWindow()

// ================================================================
// GAME TIMER
// ================================================================

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

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

        updateGame()

        panel.repaint()
      }
    }
  )

timer.start()