Code Sketch


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

// ============================================================
// STOP THE SECONDS - ULTRA ARENA X
// FULL SCALA + JAVA SWING VERSION
// COPY-PASTE READY
// NO EXTERNAL ASSETS
// ============================================================

// ============================================================
// WINDOW
// ============================================================

val SCREEN_W = 1100
val SCREEN_H = 700

val frame = new JFrame("STOP THE SECONDS - ULTRA ARENA X")

frame.setDefaultCloseOperation(
  WindowConstants.EXIT_ON_CLOSE
)

frame.setSize(
  SCREEN_W,
  SCREEN_H
)

frame.setResizable(false)

// ============================================================
// GAME SCREENS
// ============================================================

val MENU = 0
val NAME_SCREEN = 1
val TARGET_SCREEN = 2
val GAME = 3
val RESULT = 4
val LEADERBOARD = 5
val ACHIEVEMENTS = 6
val STATS = 7
val HELP = 8
val SETTINGS = 9
val PAUSE = 10

var screen = MENU

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

var playerName = ""

var score = 0
var coins = 0
var xp = 0
var level = 1

var combo = 0
var bestCombo = 0

var round = 1
var maxRounds = 10

// ============================================================
// TARGETS
// ============================================================

val targetOptions =
  Array(
    3.0,
    5.0,
    7.0,
    10.0,
    15.0
  )

var targetIndex = 2
var targetTime = targetOptions(targetIndex)

// ============================================================
// GAME MODES
// ============================================================

var difficulty = 1
// 0 EASY
// 1 NORMAL
// 2 HARD
// 3 EXTREME

var mode = 0
// 0 CLASSIC
// 1 CHALLENGE
// 2 PRACTICE

// ============================================================
// TIMER
// ============================================================

var timerRunning = false
var countdownActive = false

var startNano = 0L

var elapsed = 0.0
var countdown = 0.0

// ============================================================
// CURRENT ROUND RESULT
// ============================================================

var lastDifference = 0.0
var lastJudgement = ""

var judgementTimer = 0.0

// ============================================================
// ROUND STATS
// ============================================================

var perfects = 0
var greats = 0
var goods = 0
var misses = 0

var totalAttempts = 0

// ============================================================
// LIFETIME STATS
// ============================================================

var totalGames = 0
var totalRoundsPlayed = 0

var lifetimeScore = 0
var lifetimePerfects = 0
var lifetimeMisses = 0
var lifetimeBestCombo = 0

var fastestReaction = 999999.0
var closestHit = 999999.0

// ============================================================
// EXTRA ANIMATION STATE
// ============================================================

var animationTime = 0.0
var pulse = 0.0
var menuPulse = 0.0
var screenFlash = 0.0
var shake = 0.0

// FIXED: levelFlash IS DECLARED HERE
var levelFlash = 0.0

// ============================================================
// SETTINGS
// ============================================================

var soundEnabled = true
var particlesEnabled = true
var screenShakeEnabled = true
var animatedBackground = true

// ============================================================
// INPUT
// ============================================================

val keys =
  scala.collection.mutable.Set[Int]()

// ============================================================
// RANDOM
// ============================================================

val random =
  new Random()

// ============================================================
// LEADERBOARD DATA
// ============================================================

case class PlayerRecord(
  name: String,
  score: Int,
  level: Int,
  combo: Int,
  accuracy: Double,
  difficulty: String
)

val leaderboard =
  ArrayBuffer.empty[PlayerRecord]

// ============================================================
// ACHIEVEMENTS
// ============================================================

case class Achievement(
  name: String,
  description: String,
  var unlocked: Boolean
)

val achievements =
  ArrayBuffer(
    Achievement(
      "FIRST STEP",
      "Complete your first round",
      false
    ),
    Achievement(
      "PERFECT",
      "Hit a target perfectly",
      false
    ),
    Achievement(
      "COMBO MACHINE",
      "Reach a 5x combo",
      false
    ),
    Achievement(
      "MASTER",
      "Reach level 5",
      false
    ),
    Achievement(
      "TEN ROUNDS",
      "Complete 10 rounds",
      false
    ),
    Achievement(
      "COIN HUNTER",
      "Reach 500 coins",
      false
    ),
    Achievement(
      "PRECISION",
      "Reach 90% accuracy",
      false
    ),
    Achievement(
      "ULTRA",
      "Reach 5000 score",
      false
    )
  )

// ============================================================
// PARTICLES
// ============================================================

case class Particle(
  var x: Double,
  var y: Double,
  var vx: Double,
  var vy: Double,
  var life: Double,
  var maxLife: Double,
  var size: Double
)

val particles =
  ArrayBuffer.empty[Particle]

// ============================================================
// FLOATING TEXT
// ============================================================

case class FloatingText(
  var message: String,
  var x: Double,
  var y: Double,
  var life: Double,
  var color: Color
)

val floatingTexts =
  ArrayBuffer.empty[FloatingText]

// ============================================================
// STARS
// ============================================================

case class Star(
  var x: Double,
  var y: Double,
  var size: Double,
  var speed: Double,
  var alpha: Int
)

val stars =
  ArrayBuffer.empty[Star]

var starIndex = 0

while (starIndex < 180) {

  stars +=
    Star(
      random.nextDouble() * SCREEN_W,
      random.nextDouble() * SCREEN_H,
      1.0 + random.nextDouble() * 3.0,
      0.2 + random.nextDouble() * 0.8,
      80 + random.nextInt(170)
    )

  starIndex += 1
}

// ============================================================
// HELPER FUNCTIONS
// ============================================================

def clamp(
  value: Double,
  minimum: Double,
  maximum: Double
): Double = {

  math.max(
    minimum,
    math.min(
      maximum,
      value
    )
  )
}

def difficultyName(): String = {

  difficulty match {

    case 0 => "EASY"
    case 1 => "NORMAL"
    case 2 => "HARD"
    case 3 => "EXTREME"
    case _ => "NORMAL"
  }
}

def modeNameString(): String = {

  mode match {

    case 0 => "CLASSIC"
    case 1 => "CHALLENGE"
    case 2 => "PRACTICE"
    case _ => "CLASSIC"
  }
}

def accuracy(): Double = {

  if (totalAttempts <= 0) {

    0.0

  } else {

    (
      perfects * 100.0 +
      greats * 80.0 +
      goods * 55.0
    ) / totalAttempts.toDouble
  }
}

def beep(
  frequency: Int
): Unit = {

  if (soundEnabled) {

    try {

      Toolkit
        .getDefaultToolkit
        .beep()

    } catch {

      case _: Exception =>
    }
  }
}

// ============================================================
// PARTICLE SYSTEM
// ============================================================

def spawnBurst(
  px: Double,
  py: Double,
  requestedCount: Int
): Unit = {

  if (!particlesEnabled)
    return

  val count =
    math.min(
      requestedCount,
      100
    )

  var i = 0

  while (
    i < count &&
    particles.length < 250
  ) {

    val angle =
      random.nextDouble() *
        math.Pi * 2.0

    val speed =
      0.8 +
        random.nextDouble() * 3.8

    particles +=
      Particle(
        px,
        py,
        math.cos(angle) * speed,
        math.sin(angle) * speed,
        0.5 + random.nextDouble() * 0.8,
        0.5 + random.nextDouble() * 0.8,
        2.0 + random.nextDouble() * 6.0
      )

    i += 1
  }
}

def updateParticles(
  dt: Double
): Unit = {

  if (!particlesEnabled) {

    particles.clear()

    return
  }

  var i = particles.length - 1

  while (i >= 0) {

    val p =
      particles(i)

    p.x +=
      p.vx * 60.0 * dt

    p.y +=
      p.vy * 60.0 * dt

    p.vy +=
      0.025

    p.life -= dt

    if (p.life <= 0) {

      particles.remove(i)
    }

    i -= 1
  }
}

// ============================================================
// FLOATING TEXT
// ============================================================

def addFloatingText(
  message: String,
  px: Double,
  py: Double,
  color: Color
): Unit = {

  floatingTexts +=
    FloatingText(
      message,
      px,
      py,
      1.5,
      color
    )

  while (floatingTexts.length > 35) {

    floatingTexts.remove(0)
  }
}

def updateFloatingTexts(
  dt: Double
): Unit = {

  var i = floatingTexts.length - 1

  while (i >= 0) {

    val f =
      floatingTexts(i)

    f.y -=
      28.0 * dt

    f.life -= dt

    if (f.life <= 0) {

      floatingTexts.remove(i)
    }

    i -= 1
  }
}

// ============================================================
// GAME START
// ============================================================

def startNewGame(): Unit = {

  score = 0
  coins = 0
  xp = 0
  level = 1

  combo = 0
  bestCombo = 0

  perfects = 0
  greats = 0
  goods = 0
  misses = 0

  totalAttempts = 0

  round = 1

  elapsed = 0.0
  countdown = 0.0

  timerRunning = false
  countdownActive = false

  lastDifference = 0.0
  lastJudgement = ""

  judgementTimer = 0.0
  levelFlash = 0.0

  particles.clear()
  floatingTexts.clear()

  totalGames += 1

  screen = GAME

  spawnBurst(
    SCREEN_W / 2.0,
    350.0,
    70
  )

  beep(80)
}

// ============================================================
// RESET ROUND
// ============================================================

def resetRound(): Unit = {

  timerRunning = false
  countdownActive = false

  elapsed = 0.0
  countdown = 0.0

  lastDifference = 0.0
  lastJudgement = ""

  judgementTimer = 0.0

  particles.clear()
  floatingTexts.clear()

  spawnBurst(
    SCREEN_W / 2.0,
    350.0,
    25
  )
}

// ============================================================
// START COUNTDOWN
// ============================================================

def startCountdown(): Unit = {

  if (timerRunning)
    return

  countdown = 3.0
  countdownActive = true

  beep(60)
}

// ============================================================
// STOP TIMER
// ============================================================

def stopTimer(): Unit = {

  if (!timerRunning)
    return

  timerRunning = false

  elapsed =
    (
      System.nanoTime() -
      startNano
    ) / 1000000000.0

  evaluateRound()
}

// ============================================================
// EVALUATE ROUND
// ============================================================

def evaluateRound(): Unit = {

  totalAttempts += 1

  totalRoundsPlayed += 1

  lastDifference =
    math.abs(
      elapsed - targetTime
    )

  if (lastDifference < closestHit)
    closestHit =
      lastDifference

  if (elapsed < fastestReaction)
    fastestReaction =
      elapsed

  // Accuracy windows

  val perfectWindow =
    difficulty match {

      case 0 => 0.05
      case 1 => 0.08
      case 2 => 0.11
      case 3 => 0.14
      case _ => 0.08
    }

  val greatWindow =
    difficulty match {

      case 0 => 0.13
      case 1 => 0.20
      case 2 => 0.28
      case 3 => 0.36
      case _ => 0.20
    }

  val goodWindow =
    difficulty match {

      case 0 => 0.25
      case 1 => 0.38
      case 2 => 0.50
      case 3 => 0.65
      case _ => 0.38
    }

  // ----------------------------------------------------------
  // PERFECT
  // ----------------------------------------------------------

  if (lastDifference <= perfectWindow) {

    lastJudgement = "PERFECT"

    perfects += 1
    lifetimePerfects += 1

    combo += 1

    if (combo > bestCombo)
      bestCombo = combo

    if (bestCombo > lifetimeBestCombo)
      lifetimeBestCombo =
        bestCombo

    val multiplier =
      math.min(
        10,
        1 + combo / 2
      )

    val base =
      500 * (difficulty + 1)

    val points =
      base * multiplier

    score += points

    val coinReward =
      25 * (difficulty + 1)

    coins += coinReward

    addXP(
      40 + difficulty * 15
    )

    spawnBurst(
      SCREEN_W / 2.0,
      350.0,
      90
    )

    addFloatingText(
      "PERFECT +" + points,
      SCREEN_W / 2.0 - 80,
      270.0,
      new Color(90, 255, 170)
    )

    if (screenShakeEnabled)
      shake = 8.0

    screenFlash = 0.30

    beep(90)
  }

  // ----------------------------------------------------------
  // GREAT
  // ----------------------------------------------------------

  else if (
    lastDifference <= greatWindow
  ) {

    lastJudgement = "GREAT"

    greats += 1

    combo += 1

    if (combo > bestCombo)
      bestCombo = combo

    if (bestCombo > lifetimeBestCombo)
      lifetimeBestCombo =
        bestCombo

    val multiplier =
      math.min(
        8,
        1 + combo / 3
      )

    val points =
      300 *
        (difficulty + 1) *
        multiplier

    score += points

    coins +=
      15 * (difficulty + 1)

    addXP(28)

    spawnBurst(
      SCREEN_W / 2.0,
      350.0,
      55
    )

    addFloatingText(
      "GREAT +" + points,
      SCREEN_W / 2.0 - 65,
      270.0,
      new Color(100, 210, 255)
    )

    beep(80)
  }

  // ----------------------------------------------------------
  // GOOD
  // ----------------------------------------------------------

  else if (
    lastDifference <= goodWindow
  ) {

    lastJudgement = "GOOD"

    goods += 1

    combo += 1

    if (combo > bestCombo)
      bestCombo = combo

    val points =
      150 *
        (difficulty + 1)

    score += points

    coins +=
      8 * (difficulty + 1)

    addXP(18)

    spawnBurst(
      SCREEN_W / 2.0,
      350.0,
      30
    )

    addFloatingText(
      "GOOD +" + points,
      SCREEN_W / 2.0 - 55,
      270.0,
      new Color(255, 220, 100)
    )

    beep(70)
  }

  // ----------------------------------------------------------
  // MISS
  // ----------------------------------------------------------

  else {

    lastJudgement = "MISS"

    misses += 1
    lifetimeMisses += 1

    combo = 0

    addFloatingText(
      "MISS",
      SCREEN_W / 2.0 - 30,
      270.0,
      new Color(255, 80, 100)
    )

    if (screenShakeEnabled)
      shake = 12.0

    screenFlash = 0.18

    beep(40)
  }

  // ----------------------------------------------------------
  // UPDATE LIFETIME
  // ----------------------------------------------------------

  lifetimeScore += score

  judgementTimer = 2.2

  checkAchievements()

  screen = RESULT
}

// ============================================================
// XP SYSTEM
// ============================================================

def addXP(
  amount: Int
): Unit = {

  xp += amount

  while (xp >= level * 100) {

    xp -=
      level * 100

    level += 1

    levelFlash = 1.0

    coins += 50

    spawnBurst(
      SCREEN_W / 2.0,
      350.0,
      85
    )

    addFloatingText(
      "LEVEL UP!",
      SCREEN_W / 2.0 - 50,
      210.0,
      new Color(255, 220, 80)
    )

    beep(100)
  }
}

// ============================================================
// NEXT ROUND
// ============================================================

def nextRound(): Unit = {

  if (round >= maxRounds) {

    finishSession()

  } else {

    round += 1

    targetTime =
      targetOptions(targetIndex)

    elapsed = 0.0
    countdown = 0.0

    timerRunning = false
    countdownActive = false

    judgementTimer = 0.0
    lastJudgement = ""

    particles.clear()
    floatingTexts.clear()

    screen = GAME

    spawnBurst(
      SCREEN_W / 2.0,
      350.0,
      35
    )
  }
}

// ============================================================
// FINISH SESSION
// ============================================================

def finishSession(): Unit = {

  val record =
    PlayerRecord(
      playerName,
      score,
      level,
      bestCombo,
      accuracy(),
      difficultyName()
    )

  leaderboard += record

  // FIXED:
  // No takeInPlace with two arguments.
  // We calculate sorted top 10 and rebuild the buffer.

  val sorted =
    leaderboard
      .sortBy(
        r => -r.score
      )
      .take(10)

  leaderboard.clear()

  leaderboard ++= sorted

  checkAchievements()

  screen = MENU
}

// ============================================================
// ACHIEVEMENTS
// ============================================================

def unlockAchievement(
  achievementName: String
): Unit = {

  var i = 0

  while (i < achievements.length) {

    val a =
      achievements(i)

    if (a.name == achievementName) {

      if (!a.unlocked) {

        a.unlocked = true

        coins += 100

        addFloatingText(
          "ACHIEVEMENT: " + achievementName,
          350.0,
          180.0,
          new Color(255, 220, 80)
        )

        beep(95)
      }
    }

    i += 1
  }
}

def checkAchievements(): Unit = {

  if (totalRoundsPlayed >= 1)
    unlockAchievement(
      "FIRST STEP"
    )

  if (perfects >= 1)
    unlockAchievement(
      "PERFECT"
    )

  if (bestCombo >= 5)
    unlockAchievement(
      "COMBO MACHINE"
    )

  if (level >= 5)
    unlockAchievement(
      "MASTER"
    )

  if (totalRoundsPlayed >= 10)
    unlockAchievement(
      "TEN ROUNDS"
    )

  if (coins >= 500)
    unlockAchievement(
      "COIN HUNTER"
    )

  if (accuracy() >= 90.0)
    unlockAchievement(
      "PRECISION"
    )

  if (score >= 5000)
    unlockAchievement(
      "ULTRA"
    )
}

// ============================================================
// UPDATE STARS
// ============================================================

def updateStars(
  dt: Double
): Unit = {

  if (!animatedBackground)
    return

  var i = 0

  while (i < stars.length) {

    val s =
      stars(i)

    s.y +=
      s.speed * 12.0 * dt

    if (s.y > SCREEN_H) {

      s.y = 0.0

      s.x =
        random.nextDouble() *
          SCREEN_W
    }

    i += 1
  }
}

// ============================================================
// GAME UPDATE LOOP
// ============================================================

def updateGame(
  dt: Double
): Unit = {

  animationTime += dt
  menuPulse += dt * 2.0
  pulse += dt * 3.0

  if (screenFlash > 0.0) {

    screenFlash =
      math.max(
        0.0,
        screenFlash - dt * 3.0
      )
  }

  if (shake > 0.0) {

    shake =
      math.max(
        0.0,
        shake - dt * 16.0
      )
  }

  if (levelFlash > 0.0) {

    levelFlash =
      math.max(
        0.0,
        levelFlash - dt
      )
  }

  updateStars(dt)

  updateParticles(dt)

  updateFloatingTexts(dt)

  if (judgementTimer > 0.0) {

    judgementTimer =
      math.max(
        0.0,
        judgementTimer - dt
      )
  }

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

  if (
    screen == GAME &&
    countdownActive
  ) {

    countdown -= dt

    if (countdown <= 0.0) {

      countdownActive = false
      timerRunning = true

      startNano =
        System.nanoTime()

      elapsed = 0.0

      beep(65)
    }
  }

  if (
    screen == GAME &&
    timerRunning
  ) {

    elapsed =
      (
        System.nanoTime() -
        startNano
      ) / 1000000000.0
  }
}

// ============================================================
// UI PANEL
// ============================================================

val gamePanel =
  new JPanel {

    setPreferredSize(
      new Dimension(
        SCREEN_W,
        SCREEN_H
      )
    )

    setBackground(
      new Color(
        5,
        8,
        18
      )
    )

    setFocusable(true)

    // ========================================================
    // PAINT
    // ========================================================

    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_TEXT_ANTIALIASING,
        RenderingHints.VALUE_TEXT_ANTIALIAS_ON
      )

      var sx = 0
      var sy = 0

      if (
        screenShakeEnabled &&
        shake > 0.0
      ) {

        sx =
          ((random.nextDouble() - 0.5) * shake)
            .toInt

        sy =
          ((random.nextDouble() - 0.5) * shake)
            .toInt
      }

      g.translate(
        sx,
        sy
      )

      drawBackground(g)

      screen match {

        case MENU =>
          drawMenu(g)

        case NAME_SCREEN =>
          drawNameScreen(g)

        case TARGET_SCREEN =>
          drawTargetScreen(g)

        case GAME =>
          drawGame(g)

        case RESULT =>
          drawResult(g)

        case LEADERBOARD =>
          drawLeaderboard(g)

        case ACHIEVEMENTS =>
          drawAchievements(g)

        case STATS =>
          drawStatistics(g)

        case HELP =>
          drawHelp(g)

        case SETTINGS =>
          drawSettings(g)

        case PAUSE =>
          drawGame(g)
          drawPause(g)

        case _ =>
          drawMenu(g)
      }

      if (screenFlash > 0.0) {

        val alpha =
          math.min(
            130,
            (screenFlash * 230.0).toInt
          )

        g.setColor(
          new Color(
            255,
            255,
            255,
            alpha
          )
        )

        g.fillRect(
          -10,
          -10,
          SCREEN_W + 20,
          SCREEN_H + 20
        )
      }

      g.translate(
        -sx,
        -sy
      )
    }

    // ========================================================
    // BACKGROUND
    // ========================================================

    def drawBackground(
      g: Graphics2D
    ): Unit = {

      val gradient =
        new GradientPaint(
          0,
          0,
          new Color(
            4,
            7,
            20
          ),
          0,
          SCREEN_H,
          new Color(
            17,
            5,
            35
          )
        )

      g.setPaint(gradient)

      g.fillRect(
        0,
        0,
        SCREEN_W,
        SCREEN_H
      )

      // ------------------------------------------------------
      // STARS
      // ------------------------------------------------------

      if (animatedBackground) {

        var i = 0

        while (i < stars.length) {

          val s =
            stars(i)

          val alpha =
            clamp(
              s.alpha +
                math.sin(
                  animationTime * 2.0 +
                  s.x
                ) * 50.0,
              20.0,
              255.0
            ).toInt

          g.setColor(
            new Color(
              160,
              220,
              255,
              alpha
            )
          )

          g.fillOval(
            s.x.toInt,
            s.y.toInt,
            math.max(
              1,
              s.size.toInt
            ),
            math.max(
              1,
              s.size.toInt
            )
          )

          i += 1
        }
      }

      // ------------------------------------------------------
      // GRID
      // ------------------------------------------------------

      g.setColor(
        new Color(
          50,
          120,
          160,
          42
        )
      )

      val offset =
        if (animatedBackground)
          ((animationTime * 24.0) % 50.0).toInt
        else
          0

      var gx = -50 + offset

      while (
        gx < SCREEN_W + 50
      ) {

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

        gx += 50
      }

      var gy = -50 + offset

      while (
        gy < SCREEN_H + 50
      ) {

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

        gy += 50
      }

      // ------------------------------------------------------
      // CENTRAL GLOW
      // ------------------------------------------------------

      val glowRadius =
        (
          280.0 +
          math.sin(
            animationTime * 2.0
          ) * 30.0
        ).toFloat

      val radial =
        new RadialGradientPaint(
          new Point(
            SCREEN_W / 2,
            SCREEN_H / 2
          ),
          glowRadius,
          Array(
            0.0f,
            0.45f,
            1.0f
          ),
          Array(
            new Color(
              50,
              120,
              255,
              35
            ),
            new Color(
              120,
              20,
              220,
              12
            ),
            new Color(
              0,
              0,
              0,
              0
            )
          )
        )

      g.setPaint(radial)

      g.fillRect(
        0,
        0,
        SCREEN_W,
        SCREEN_H
      )
    }

    // ========================================================
    // TEXT
    // ========================================================

    def centered(
      g: Graphics2D,
      text: String,
      y: Int,
      size: Int,
      color: Color
    ): Unit = {

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

      g.setColor(color)

      val width =
        g.getFontMetrics.stringWidth(text)

      g.drawString(
        text,
        (SCREEN_W - width) / 2,
        y
      )
    }

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

    def panelBox(
      g: Graphics2D,
      x: Int,
      y: Int,
      w: Int,
      h: Int
    ): Unit = {

      g.setColor(
        new Color(
          10,
          15,
          35,
          235
        )
      )

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

      g.setColor(
        new Color(
          80,
          180,
          255,
          150
        )
      )

      g.setStroke(
        new BasicStroke(
          2.0f
        )
      )

      g.drawRoundRect(
        x,
        y,
        w,
        h,
        24,
        24
      )
    }

    // ========================================================
    // BUTTON
    // ========================================================

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

      g.setColor(
        if (active)
          new Color(
            30,
            145,
            235,
            210
          )
        else
          new Color(
            18,
            30,
            65,
            225
          )
      )

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

      g.setColor(
        if (active)
          new Color(
            120,
            235,
            255
          )
        else
          new Color(
            75,
            120,
            180
          )
      )

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

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

      val width =
        g.getFontMetrics
          .stringWidth(text)

      g.setColor(
        Color.WHITE
      )

      g.drawString(
        text,
        x + (w - width) / 2,
        y + h / 2 + 6
      )
    }

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

    def drawMenu(
      g: Graphics2D
    ): Unit = {

      val titleY =
        110 +
          (
            math.sin(
              menuPulse
            ) * 5.0
          ).toInt

      centered(
        g,
        "STOP THE SECONDS",
        titleY,
        52,
        new Color(
          100,
          225,
          255
        )
      )

      centered(
        g,
        "ULTRA ARENA X",
        titleY + 55,
        28,
        new Color(
          220,
          100,
          255
        )
      )

      centered(
        g,
        "PRECISION ? COMBO ? LEVEL ? ACHIEVEMENTS",
        titleY + 88,
        14,
        new Color(
          160,
          190,
          220
        )
      )

      drawButton(
        g,
        "PLAY",
        390,
        250,
        320,
        55,
        true
      )

      drawButton(
        g,
        "LEADERBOARD",
        390,
        320,
        320,
        50,
        false
      )

      drawButton(
        g,
        "ACHIEVEMENTS",
        390,
        385,
        320,
        50,
        false
      )

      drawButton(
        g,
        "STATISTICS",
        390,
        450,
        320,
        50,
        false
      )

      drawButton(
        g,
        "SETTINGS",
        390,
        515,
        155,
        45,
        false
      )

      drawButton(
        g,
        "HELP",
        555,
        515,
        155,
        45,
        false
      )

      centered(
        g,
        "ENTER = PLAY    L = LEADERBOARD    H = HELP",
        615,
        13,
        new Color(
          120,
          150,
          180
        )
      )
    }

    // ========================================================
    // NAME SCREEN
    // ========================================================

    def drawNameScreen(
      g: Graphics2D
    ): Unit = {

      centered(
        g,
        "CREATE YOUR PLAYER",
        120,
        34,
        new Color(
          100,
          220,
          255
        )
      )

      panelBox(
        g,
        270,
        185,
        560,
        195
      )

      centered(
        g,
        if (playerName.isEmpty)
          "TYPE YOUR NAME"
        else
          playerName,
        270,
        30,
        Color.WHITE
      )

      centered(
        g,
        "ENTER = CONTINUE",
        330,
        15,
        new Color(
          100,
          220,
          255
        )
      )

      centered(
        g,
        "BACKSPACE = DELETE",
        360,
        13,
        new Color(
          150,
          165,
          190
        )
      )

      centered(
        g,
        "ESC = MENU",
        610,
        13,
        new Color(
          120,
          135,
          160
        )
      )
    }

    // ========================================================
    // TARGET SCREEN
    // ========================================================

    def drawTargetScreen(
      g: Graphics2D
    ): Unit = {

      centered(
        g,
        "TARGET CONFIGURATION",
        80,
        34,
        new Color(
          255,
          215,
          95
        )
      )

      centered(
        g,
        "PLAYER: " + playerName,
        115,
        16,
        Color.WHITE
      )

      var y = 155
      var i = 0

      while (
        i < targetOptions.length
      ) {

        drawButton(
          g,
          f"${targetOptions(i)}%.2f SECONDS",
          380,
          y,
          340,
          48,
          i == targetIndex
        )

        y += 58
        i += 1
      }

      centered(
        g,
        "UP / DOWN OR W / S = TARGET",
        465,
        14,
        new Color(
          150,
          200,
          230
        )
      )

      centered(
        g,
        "D = DIFFICULTY     M = MODE",
        495,
        13,
        new Color(
          160,
          175,
          195
        )
      )

      centered(
        g,
        "ENTER = START",
        530,
        16,
        new Color(
          100,
          255,
          180
        )
      )

      centered(
        g,
        "DIFFICULTY: " +
          difficultyName() +
          "     MODE: " +
          modeNameString(),
        580,
        15,
        new Color(
          255,
          170,
          100
        )
      )
    }

    // ========================================================
    // GAME SCREEN
    // ========================================================

    def drawGame(
      g: Graphics2D
    ): Unit = {

      // ------------------------------------------------------
      // TOP BAR
      // ------------------------------------------------------

      g.setColor(
        new Color(
          7,
          11,
          28,
          245
        )
      )

      g.fillRect(
        0,
        0,
        SCREEN_W,
        82
      )

      g.setColor(
        new Color(
          70,
          180,
          255,
          120
        )
      )

      g.drawLine(
        0,
        82,
        SCREEN_W,
        82
      )

      // Player

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

      g.setColor(
        Color.WHITE
      )

      g.drawString(
        playerName,
        28,
        30
      )

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

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

      g.drawString(
        "LEVEL " + level,
        28,
        51
      )

      // Score

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

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

      g.drawString(
        "SCORE " + score,
        180,
        30
      )

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

      g.setColor(
        new Color(
          120,
          230,
          170
        )
      )

      g.drawString(
        "COINS " + coins,
        180,
        51
      )

      // Combo

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

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

      g.drawString(
        "COMBO x" + combo,
        315,
        30
      )

      // Round

      g.setColor(
        new Color(
          120,
          200,
          255
        )
      )

      g.drawString(
        "ROUND " +
          round +
          "/" +
          maxRounds,
        500,
        30
      )

      // Target

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

      g.setColor(Color.WHITE)

      g.drawString(
        f"TARGET  $targetTime%.2f s",
        500,
        51
      )

      // XP BAR

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

      g.fillRoundRect(
        650,
        25,
        190,
        11,
        8,
        8
      )

      val needed =
        math.max(
          1,
          level * 100
        )

      val ratio =
        clamp(
          xp.toDouble /
            needed.toDouble,
          0.0,
          1.0
        )

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

      g.fillRoundRect(
        650,
        25,
        (190.0 * ratio).toInt,
        11,
        8,
        8
      )

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

      g.setColor(
        new Color(
          165,
          185,
          215
        )
      )

      g.drawString(
        "XP " +
          xp +
          "/" +
          needed,
        650,
        54
      )

      g.drawString(
        difficultyName(),
        910,
        30
      )

      g.drawString(
        modeNameString(),
        910,
        52
      )

      // ------------------------------------------------------
      // MAIN ARENA
      // ------------------------------------------------------

      val centerX =
        SCREEN_W / 2

      val centerY =
        350

      val arenaRadius =
        (
          180.0 +
          math.sin(pulse) * 10.0
        ).toInt

      // Outer rings

      var ring =
        arenaRadius + 55

      while (ring > arenaRadius - 5) {

        val alpha =
          clamp(
            90.0 -
              (ring - arenaRadius) *
                2.0,
            0.0,
            90.0
          ).toInt

        g.setColor(
          new Color(
            60,
            180,
            255,
            alpha
          )
        )

        g.drawOval(
          centerX - ring,
          centerY - ring,
          ring * 2,
          ring * 2
        )

        ring -= 8
      }

      // Arena body

      g.setColor(
        new Color(
          6,
          20,
          42,
          230
        )
      )

      g.fillOval(
        centerX - arenaRadius,
        centerY - arenaRadius,
        arenaRadius * 2,
        arenaRadius * 2
      )

      g.setColor(
        new Color(
          80,
          200,
          255
        )
      )

      g.setStroke(
        new BasicStroke(
          3.0f
        )
      )

      g.drawOval(
        centerX - arenaRadius,
        centerY - arenaRadius,
        arenaRadius * 2,
        arenaRadius * 2
      )

      // Target ring

      val targetRadius =
        92 +
          (
            math.sin(
              animationTime * 3.0
            ) * 5.0
          ).toInt

      g.setColor(
        new Color(
          190,
          90,
          255,
          150
        )
      )

      g.setStroke(
        new BasicStroke(
          4.0f
        )
      )

      g.drawOval(
        centerX - targetRadius,
        centerY - targetRadius,
        targetRadius * 2,
        targetRadius * 2
      )

      // Additional rotating target marks

      var mark = 0

      while (mark < 8) {

        val ang =
          animationTime +
            mark *
              math.Pi /
              4.0

        val mx =
          (
            centerX +
            math.cos(ang) *
              targetRadius
          ).toInt

        val my =
          (
            centerY +
            math.sin(ang) *
              targetRadius
          ).toInt

        g.setColor(
          new Color(
            120,
            240,
            255
          )
        )

        g.fillOval(
          mx - 4,
          my - 4,
          8,
          8
        )

        mark += 1
      }

      // Core

      val coreRadius =
        22 +
          (
            math.sin(
              animationTime * 5.0
            ) * 4.0
          ).toInt

      g.setColor(
        new Color(
          110,
          235,
          255,
          220
        )
      )

      g.fillOval(
        centerX - coreRadius,
        centerY - coreRadius,
        coreRadius * 2,
        coreRadius * 2
      )

      g.setColor(Color.WHITE)

      g.drawOval(
        centerX - coreRadius - 4,
        centerY - coreRadius - 4,
        (coreRadius + 4) * 2,
        (coreRadius + 4) * 2
      )

      // ------------------------------------------------------
      // COUNTDOWN / TIMER
      // ------------------------------------------------------

      if (countdownActive) {

        centered(
          g,
          math.ceil(
            countdown
          ).toInt.toString,
          355,
          68,
          new Color(
            255,
            220,
            90
          )
        )

      } else {

        centered(
          g,
          f"$elapsed%.2f",
          355,
          60,
          Color.WHITE
        )

        centered(
          g,
          if (timerRunning)
            "STOP THE CLOCK"
          else
            "READY",
          390,
          15,
          new Color(
            120,
            220,
            255
          )
        )
      }

      // ------------------------------------------------------
      // JUDGEMENT
      // ------------------------------------------------------

      if (judgementTimer > 0.0) {

        val judgementColor =
          lastJudgement match {

            case "PERFECT" =>
              new Color(
                100,
                255,
                170
              )

            case "GREAT" =>
              new Color(
                100,
                210,
                255
              )

            case "GOOD" =>
              new Color(
                255,
                220,
                100
              )

            case _ =>
              new Color(
                255,
                90,
                100
              )
          }

        centered(
          g,
          lastJudgement,
          470,
          32,
          judgementColor
        )

        centered(
          g,
          f"ERROR  $lastDifference%.3f s",
          500,
          14,
          new Color(
            180,
            190,
            210
          )
        )
      }

      // ------------------------------------------------------
      // LEVEL FLASH
      // ------------------------------------------------------

      if (levelFlash > 0.0) {

        val alpha =
          clamp(
            levelFlash * 180.0,
            0.0,
            180.0
          ).toInt

        g.setColor(
          new Color(
            255,
            220,
            80,
            alpha
          )
        )

        g.setStroke(
          new BasicStroke(
            4.0f
          )
        )

        g.drawOval(
          centerX - 150,
          centerY - 150,
          300,
          300
        )
      }

      // ------------------------------------------------------
      // PARTICLES
      // ------------------------------------------------------

      if (particlesEnabled) {

        var i = 0

        while (
          i < particles.length
        ) {

          val p =
            particles(i)

          val alpha =
            clamp(
              (
                p.life /
                p.maxLife
              ) * 255.0,
              0.0,
              255.0
            ).toInt

          g.setColor(
            new Color(
              110,
              220,
              255,
              alpha
            )
          )

          val s =
            math.max(
              1,
              p.size.toInt
            )

          g.fillOval(
            p.x.toInt,
            p.y.toInt,
            s,
            s
          )

          i += 1
        }
      }

      // ------------------------------------------------------
      // FLOATING TEXT
      // ------------------------------------------------------

      var fi = 0

      while (
        fi < floatingTexts.length
      ) {

        val f =
          floatingTexts(fi)

        val alpha =
          clamp(
            f.life * 255.0,
            0.0,
            255.0
          ).toInt

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

        g.setColor(
          new Color(
            f.color.getRed,
            f.color.getGreen,
            f.color.getBlue,
            alpha
          )
        )

        g.drawString(
          f.message,
          f.x.toInt,
          f.y.toInt
        )

        fi += 1
      }

      // ------------------------------------------------------
      // HELP TEXT
      // ------------------------------------------------------

      centered(
        g,
        if (countdownActive)
          "GET READY..."
        else if (timerRunning)
          "SPACE = STOP"
        else
          "SPACE = START",
        555,
        17,
        new Color(
          100,
          230,
          255
        )
      )

      centered(
        g,
        "P = PAUSE     R = RESET     L = LEADERBOARD     H = HELP",
        590,
        13,
        new Color(
          135,
          155,
          180
        )
      )

      centered(
        g,
        "ESC = MENU",
        620,
        13,
        new Color(
          120,
          135,
          160
        )
      )
    }

    // ========================================================
    // RESULT
    // ========================================================

    def drawResult(
      g: Graphics2D
    ): Unit = {

      centered(
        g,
        "ROUND COMPLETE",
        90,
        38,
        new Color(
          100,
          225,
          255
        )
      )

      panelBox(
        g,
        250,
        135,
        600,
        390
      )

      centered(
        g,
        lastJudgement,
        225,
        40,
        lastJudgement match {

          case "PERFECT" =>
            new Color(
              100,
              255,
              170
            )

          case "GREAT" =>
            new Color(
              100,
              210,
              255
            )

          case "GOOD" =>
            new Color(
              255,
              220,
              100
            )

          case _ =>
            new Color(
              255,
              90,
              100
            )
        }
      )

      centered(
        g,
        f"ERROR  $lastDifference%.3f seconds",
        270,
        17,
        Color.WHITE
      )

      centered(
        g,
        "SCORE: " + score,
        315,
        22,
        new Color(
          255,
          220,
          90
        )
      )

      centered(
        g,
        "COMBO: x" + combo,
        350,
        18,
        new Color(
          255,
          110,
          220
        )
      )

      centered(
        g,
        "LEVEL: " + level,
        385,
        18,
        new Color(
          100,
          210,
          255
        )
      )

      centered(
        g,
        f"ACCURACY: ${accuracy()}%.1f%%",
        420,
        18,
        new Color(
          120,
          230,
          170
        )
      )

      centered(
        g,
        "ENTER = NEXT ROUND",
        470,
        16,
        Color.WHITE
      )

      centered(
        g,
        "L = LEADERBOARD     ESC = MENU",
        510,
        13,
        new Color(
          140,
          160,
          190
        )
      )
    }

    // ========================================================
    // LEADERBOARD
    // ========================================================

    def drawLeaderboard(
      g: Graphics2D
    ): Unit = {

      centered(
        g,
        "SESSION LEADERBOARD",
        75,
        34,
        new Color(
          255,
          220,
          100
        )
      )

      val sorted =
        leaderboard
          .sortBy(
            p => -p.score
          )
          .take(10)

      if (sorted.isEmpty) {

        centered(
          g,
          "NO SCORES YET",
          300,
          24,
          new Color(
            150,
            170,
            200
          )
        )

      } else {

        var y = 125
        var rank = 1

        sorted.foreach { p =>

          panelBox(
            g,
            160,
            y - 25,
            780,
            52
          )

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

          g.setColor(
            if (rank == 1)
              new Color(
                255,
                215,
                80
              )
            else if (rank == 2)
              new Color(
                210,
                220,
                235
              )
            else if (rank == 3)
              new Color(
                220,
                160,
                100
              )
            else
              Color.WHITE
          )

          g.drawString(
            "#" + rank,
            185,
            y + 5
          )

          g.setColor(Color.WHITE)

          g.drawString(
            p.name,
            250,
            y + 5
          )

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

          g.drawString(
            "SCORE " + p.score,
            500,
            y + 5
          )

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

          g.drawString(
            f"${p.accuracy}%.1f%%",
            680,
            y + 5
          )

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

          g.drawString(
            "LVL " + p.level,
            790,
            y + 5
          )

          rank += 1
          y += 58
        }
      }

      centered(
        g,
        "ESC = MENU",
        645,
        14,
        new Color(
          130,
          150,
          180
        )
      )
    }

    // ========================================================
    // ACHIEVEMENTS
    // ========================================================

    def drawAchievements(
      g: Graphics2D
    ): Unit = {

      centered(
        g,
        "ACHIEVEMENTS",
        75,
        34,
        new Color(
          255,
          215,
          100
        )
      )

      var y = 115

      achievements.foreach { a =>

        panelBox(
          g,
          220,
          y - 25,
          660,
          55
        )

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

        g.setColor(
          if (a.unlocked)
            new Color(
              100,
              255,
              180
            )
          else
            new Color(
              100,
              110,
              130
            )
        )

        g.drawString(
          if (a.unlocked)
            "?"
          else
            "?",
          245,
          y + 4
        )

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

        g.setColor(Color.WHITE)

        g.drawString(
          a.name,
          285,
          y
        )

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

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

        g.drawString(
          a.description,
          285,
          y + 20
        )

        y += 65
      }

      centered(
        g,
        "ESC = MENU",
        650,
        14,
        new Color(
          130,
          150,
          180
        )
      )
    }

    // ========================================================
    // STATISTICS
    // ========================================================

    def drawStatistics(
      g: Graphics2D
    ): Unit = {

      centered(
        g,
        "PLAYER STATISTICS",
        80,
        34,
        new Color(
          100,
          220,
          255
        )
      )

      panelBox(
        g,
        260,
        120,
        580,
        450
      )

      val fastest =
        if (fastestReaction > 100000)
          "--"
        else
          f"$fastestReaction%.3f s"

      val closest =
        if (closestHit > 100000)
          "--"
        else
          f"$closestHit%.3f s"

      val stats =
        Array(
          "PLAYER: " + playerName,
          "LEVEL: " + level,
          "CURRENT SCORE: " + score,
          "LIFETIME SCORE: " + lifetimeScore,
          "TOTAL GAMES: " + totalGames,
          "ROUNDS PLAYED: " + totalRoundsPlayed,
          "PERFECTS: " + lifetimePerfects,
          "MISSES: " + lifetimeMisses,
          "BEST COMBO: x" + lifetimeBestCombo,
          "COINS: " + coins,
          "FASTEST: " + fastest,
          "CLOSEST HIT: " + closest,
          f"CURRENT ACCURACY: ${accuracy()}%.1f%%"
        )

      var y = 160

      stats.foreach { line =>

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

        g.setColor(Color.WHITE)

        g.drawString(
          line,
          325,
          y
        )

        y += 30
      }

      centered(
        g,
        "ESC = MENU",
        640,
        14,
        new Color(
          130,
          150,
          180
        )
      )
    }

    // ========================================================
    // HELP
    // ========================================================

    def drawHelp(
      g: Graphics2D
    ): Unit = {

      centered(
        g,
        "HOW TO PLAY",
        72,
        36,
        new Color(
          100,
          220,
          255
        )
      )

      panelBox(
        g,
        160,
        105,
        780,
        500
      )

      val help =
        Array(
          "SPACE   Start / Stop the timer",
          "UP/DOWN   Select target",
          "W/S       Select target",
          "D         Change difficulty",
          "M         Change mode",
          "P         Pause",
          "R         Reset round",
          "L         Leaderboard",
          "H         Help",
          "ESC       Return to menu",
          "",
          "PERFECT = closest timing",
          "GREAT   = very close",
          "GOOD    = acceptable timing",
          "MISS    = outside timing window",
          "",
          "Higher difficulty = smaller timing window",
          "Combo increases your score multiplier",
          "Successful rounds give XP and coins",
          "Level-ups grant bonus coins",
          "Achievements grant bonus coins"
        )

      var y = 145

      help.foreach { line =>

        g.setFont(
          new Font(
            "SansSerif",
            if (line.contains("="))
              Font.BOLD
            else
              Font.PLAIN,
            15
          )
        )

        g.setColor(
          if (line.startsWith("PERFECT"))
            new Color(
              100,
              255,
              170
            )
          else if (
            line.startsWith("GREAT")
          )
            new Color(
              100,
              210,
              255
            )
          else if (
            line.startsWith("GOOD")
          )
            new Color(
              255,
              220,
              100
            )
          else if (
            line.startsWith("MISS")
          )
            new Color(
              255,
              100,
              110
            )
          else
            Color.WHITE
        )

        g.drawString(
          line,
          220,
          y
        )

        y += 21
      }

      centered(
        g,
        "ESC = MENU",
        645,
        14,
        new Color(
          130,
          150,
          180
        )
      )
    }

    // ========================================================
    // SETTINGS
    // ========================================================

    def drawSettings(
      g: Graphics2D
    ): Unit = {

      centered(
        g,
        "SETTINGS",
        80,
        36,
        new Color(
          190,
          120,
          255
        )
      )

      panelBox(
        g,
        300,
        140,
        500,
        390
      )

      setting(
        g,
        "1. SOUND",
        soundEnabled,
        220
      )

      setting(
        g,
        "2. PARTICLES",
        particlesEnabled,
        285
      )

      setting(
        g,
        "3. SCREEN SHAKE",
        screenShakeEnabled,
        350
      )

      setting(
        g,
        "4. ANIMATED BACKGROUND",
        animatedBackground,
        415
      )

      centered(
        g,
        "Press 1 / 2 / 3 / 4 to toggle",
        560,
        14,
        new Color(
          155,
          175,
          205
        )
      )

      centered(
        g,
        "ESC = MENU",
        610,
        14,
        new Color(
          130,
          150,
          180
        )
      )
    }

    def setting(
      g: Graphics2D,
      label: String,
      enabled: Boolean,
      y: Int
    ): Unit = {

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

      g.setColor(
        Color.WHITE
      )

      g.drawString(
        label,
        350,
        y
      )

      g.setColor(
        if (enabled)
          new Color(
            100,
            255,
            180
          )
        else
          new Color(
            255,
            100,
            110
          )
      )

      g.drawString(
        if (enabled)
          "ON"
        else
          "OFF",
        700,
        y
      )
    }

    // ========================================================
    // PAUSE
    // ========================================================

    def drawPause(
      g: Graphics2D
    ): Unit = {

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

      g.fillRect(
        0,
        0,
        SCREEN_W,
        SCREEN_H
      )

      centered(
        g,
        "PAUSED",
        300,
        48,
        new Color(
          100,
          220,
          255
        )
      )

      centered(
        g,
        "P OR SPACE = RESUME",
        355,
        18,
        Color.WHITE
      )

      centered(
        g,
        "ESC = MENU",
        390,
        14,
        new Color(
          150,
          165,
          190
        )
      )
    }
  }

// ============================================================
// ATTACH PANEL
// ============================================================

frame.setContentPane(
  gamePanel
)

// ============================================================
// KEYBOARD INPUT
// ============================================================

gamePanel.addKeyListener(
  new KeyAdapter {

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

      val key =
        e.getKeyCode

      keys += key

      handleKeyPressed(
        key
      )
    }

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

      keys -= e.getKeyCode
    }

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

      handleKeyTyped(
        e.getKeyChar
      )
    }
  }
)

// ============================================================
// KEY PRESSED HANDLER
// ============================================================

def handleKeyPressed(
  key: Int
): Unit = {

  // ----------------------------------------------------------
  // ESC
  // ----------------------------------------------------------

  if (
    key == KeyEvent.VK_ESCAPE
  ) {

    if (
      screen == GAME ||
      screen == PAUSE
    ) {

      timerRunning = false
      countdownActive = false
      screen = MENU

    } else {

      screen = MENU
    }

    gamePanel.repaint()

    return
  }

  // ----------------------------------------------------------
  // MENU
  // ----------------------------------------------------------

  if (screen == MENU) {

    key match {

      case KeyEvent.VK_ENTER =>
        playerName = ""
        screen = NAME_SCREEN

      case KeyEvent.VK_L =>
        screen = LEADERBOARD

      case KeyEvent.VK_A =>
        screen = ACHIEVEMENTS

      case KeyEvent.VK_S =>
        screen = STATS

      case KeyEvent.VK_H =>
        screen = HELP

      case KeyEvent.VK_O =>
        screen = SETTINGS

      case _ =>
    }

    return
  }

  // ----------------------------------------------------------
  // NAME SCREEN
  // ----------------------------------------------------------

  if (
    screen == NAME_SCREEN
  ) {

    if (
      key == KeyEvent.VK_ENTER
    ) {

      if (
        playerName
          .trim
          .nonEmpty
      ) {

        playerName =
          playerName
            .trim
            .take(18)

        screen =
          TARGET_SCREEN
      }

    } else if (
      key == KeyEvent.VK_BACK_SPACE
    ) {

      if (
        playerName.nonEmpty
      ) {

        playerName =
          playerName.dropRight(1)
      }
    }

    gamePanel.repaint()

    return
  }

  // ----------------------------------------------------------
  // TARGET SCREEN
  // ----------------------------------------------------------

  if (
    screen == TARGET_SCREEN
  ) {

    key match {

      case KeyEvent.VK_UP |
           KeyEvent.VK_W =>

        targetIndex =
          math.max(
            0,
            targetIndex - 1
          )

        targetTime =
          targetOptions(targetIndex)

      case KeyEvent.VK_DOWN |
           KeyEvent.VK_S =>

        targetIndex =
          math.min(
            targetOptions.length - 1,
            targetIndex + 1
          )

        targetTime =
          targetOptions(targetIndex)

      case KeyEvent.VK_D =>

        difficulty =
          (difficulty + 1) % 4

      case KeyEvent.VK_M =>

        mode =
          (mode + 1) % 3

      case KeyEvent.VK_ENTER =>

        startNewGame()

      case _ =>
    }

    gamePanel.repaint()

    return
  }

  // ----------------------------------------------------------
  // GAME
  // ----------------------------------------------------------

  if (
    screen == GAME
  ) {

    key match {

      case KeyEvent.VK_SPACE =>

        if (!countdownActive) {

          if (!timerRunning)
            startCountdown()
          else
            stopTimer()
        }

      case KeyEvent.VK_P =>

        timerRunning = false
        countdownActive = false
        screen = PAUSE

      case KeyEvent.VK_R =>

        resetRound()

      case KeyEvent.VK_L =>

        timerRunning = false
        countdownActive = false
        screen = LEADERBOARD

      case KeyEvent.VK_H =>

        timerRunning = false
        countdownActive = false
        screen = HELP

      case _ =>
    }

    gamePanel.repaint()

    return
  }

  // ----------------------------------------------------------
  // PAUSE
  // ----------------------------------------------------------

  if (
    screen == PAUSE
  ) {

    if (
      key == KeyEvent.VK_P ||
      key == KeyEvent.VK_SPACE
    ) {

      screen = GAME
    }

    gamePanel.repaint()

    return
  }

  // ----------------------------------------------------------
  // RESULT
  // ----------------------------------------------------------

  if (
    screen == RESULT
  ) {

    key match {

      case KeyEvent.VK_ENTER =>
        nextRound()

      case KeyEvent.VK_L =>
        screen = LEADERBOARD

      case _ =>
    }

    gamePanel.repaint()

    return
  }

  // ----------------------------------------------------------
  // SETTINGS
  // ----------------------------------------------------------

  if (
    screen == SETTINGS
  ) {

    key match {

      case KeyEvent.VK_1 =>
        soundEnabled = !soundEnabled

      case KeyEvent.VK_2 =>
        particlesEnabled = !particlesEnabled

        if (!particlesEnabled)
          particles.clear()

      case KeyEvent.VK_3 =>
        screenShakeEnabled =
          !screenShakeEnabled

      case KeyEvent.VK_4 =>
        animatedBackground =
          !animatedBackground

      case _ =>
    }

    gamePanel.repaint()

    return
  }

  gamePanel.repaint()
}

// ============================================================
// TEXT INPUT HANDLER
// ============================================================

def handleKeyTyped(
  ch: Char
): Unit = {

  if (
    screen == NAME_SCREEN
  ) {

    if (
      ch >= ' ' &&
      ch <= '~'
    ) {

      if (
        playerName.length < 18
      ) {

        playerName += ch
      }
    }

    gamePanel.repaint()
  }
}

// ============================================================
// MOUSE INPUT
// ============================================================

gamePanel.addMouseListener(
  new MouseAdapter {

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

      handleMouse(
        e.getX,
        e.getY
      )
    }
  }
)

// ============================================================
// MOUSE HANDLER
// ============================================================

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

  // ----------------------------------------------------------
  // MENU BUTTONS
  // ----------------------------------------------------------

  if (screen == MENU) {

    if (
      x >= 390 &&
      x <= 710 &&
      y >= 250 &&
      y <= 305
    ) {

      playerName = ""
      screen = NAME_SCREEN

    } else if (
      x >= 390 &&
      x <= 710 &&
      y >= 320 &&
      y <= 370
    ) {

      screen = LEADERBOARD

    } else if (
      x >= 390 &&
      x <= 710 &&
      y >= 385 &&
      y <= 435
    ) {

      screen = ACHIEVEMENTS

    } else if (
      x >= 390 &&
      x <= 710 &&
      y >= 450 &&
      y <= 500
    ) {

      screen = STATS

    } else if (
      x >= 390 &&
      x <= 545 &&
      y >= 515 &&
      y <= 560
    ) {

      screen = SETTINGS

    } else if (
      x >= 555 &&
      x <= 710 &&
      y >= 515 &&
      y <= 560
    ) {

      screen = HELP
    }
  }

  // ----------------------------------------------------------
  // GAME CLICK
  // ----------------------------------------------------------

  else if (
    screen == GAME
  ) {

    val dx =
      x -
        SCREEN_W / 2.0

    val dy =
      y -
        350.0

    val distance =
      math.hypot(
        dx,
        dy
      )

    if (
      distance < 220
    ) {

      if (
        !countdownActive
      ) {

        if (
          !timerRunning
        ) {

          startCountdown()

        } else {

          stopTimer()
        }
      }
    }
  }

  // ----------------------------------------------------------
  // OTHER SCREENS
  // ----------------------------------------------------------

  else if (
    screen == LEADERBOARD ||
    screen == ACHIEVEMENTS ||
    screen == STATS ||
    screen == HELP ||
    screen == SETTINGS
  ) {

    if (y > 580) {

      screen = MENU
    }
  }

  gamePanel.requestFocusInWindow()
  gamePanel.repaint()
}

// ============================================================
// MAIN SWING TIMER
// ============================================================

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

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

        updateGame(
          0.016
        )

        gamePanel.repaint()
      }
    }
  )

swingTimer.start()

// ============================================================
// INITIAL WINDOW
// ============================================================

frame.setLocationRelativeTo(null)

frame.setVisible(true)

gamePanel.requestFocusInWindow()

println(
  "=================================================="
)

println(
  " STOP THE SECONDS - ULTRA ARENA X"
)

println(
  " FULL SCALA SWING VERSION LOADED"
)

println(
  "=================================================="
)