Code Sketch


yoiiiiii
By: Mhalsakant School
Category: Programming
import javax.swing._
import java.awt._
import java.awt.event._
import java.awt.geom._
import java.io._
import java.net._
import java.util.concurrent._
import scala.collection.mutable
import scala.util.Random

object RealisticDriveOnline_Cockpit {

  val PORT = 5555

  def main(args: Array[String]): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = new DriveFrame(args)
    })
  }
}

class DriveFrame(args: Array[String]) extends JFrame {
  setTitle("REALISTIC DRIVE ONLINE - COCKPIT EDITION")
  setSize(1280, 820)
  setResizable(false)
  setLocationRelativeTo(null)
  setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
  val panel = new DriveGamePanel(args)
  setContentPane(panel)
  setVisible(true)
  panel.requestFocusInWindow()
}

class DriveGamePanel(args: Array[String]) extends JPanel {

  setFocusable(true)

  // ==========================================================
  // GAME STATES
  // ==========================================================
  val MENU = 0
  val GAME = 1
  val PAUSE = 2
  val GAME_OVER = 3
  val FINISH = 4

  var state = MENU

  // ==========================================================
  // PLAYER PROFILE
  // ==========================================================
  var playerName = "Player"
  var playerId = -1

  // ==========================================================
  // NETWORK CLIENT
  // ==========================================================
  var socket: Socket = null
  var input: BufferedReader = null
  var output: PrintWriter = null
  var online = false
  var serverHost = "127.0.0.1"

  case class RemotePlayer(
      var name: String,
      var x: Double,
      var distance: Double,
      var speed: Double,
      var gear: String,
      var score: Int,
      var race: Boolean,
      var colorCode: Int
  )

  val onlinePlayers = mutable.Map[Int, RemotePlayer]()

  // ==========================================================
  // CHAT
  // ==========================================================
  val chatLines = mutable.ArrayBuffer[String]()
  var showChat = true

  def addChat(text: String): Unit = synchronized {
    chatLines += text
    while (chatLines.length > 8) chatLines.remove(0)
  }

  // ==========================================================
  // BUILT-IN SERVER
  // ==========================================================
  case class ServerPlayer(
      id: Int,
      socket: Socket,
      input: BufferedReader,
      output: PrintWriter,
      var name: String,
      var x: Double,
      var distance: Double,
      var speed: Double,
      var gear: String,
      var score: Int,
      var race: Boolean,
      var colorCode: Int
  )

  val serverPlayers = new ConcurrentHashMap[Int, ServerPlayer]()
  val serverPool = Executors.newCachedThreadPool()
  var serverSocket: ServerSocket = null
  var serverRunning = false
  var nextServerId = 1000

  // ==========================================================
  // GAME VARIABLES
  // ==========================================================
  val rnd = new Random()

  var carX = 0.0
  var speed = 0.0
  var rpm = 900.0
  var steering = 0.0
  var gear = "N"
  var fuel = 100.0
  var damage = 0.0
  var nitro = 100.0
  var distance = 0.0
  var score = 0
  val targetDistance = 5000.0

  var roadScroll = 0.0
  var headlights = false
  var cockpit = true
  var mapVisible = true
  var smoke = 0.0
  var crashFlash = 0.0
  var hornFlash = 0.0
  var raceMode = false
  var raceCountdown = 0
  var raceStarted = false
  var message = ""
  var messageTimer = 0.0

  // ==========================================================
  // LOCAL TRAFFIC
  // ==========================================================
  case class Traffic(
      var lane: Int,
      var z: Double,
      var aiSpeed: Double,
      var kind: Int,
      var color: Color
  )

  val traffic = mutable.ArrayBuffer[Traffic]()

  // ==========================================================
  // INPUT
  // ==========================================================
  val keys = mutable.Set[Int]()

  val controlKeys = Array(
    KeyEvent.VK_LEFT,
    KeyEvent.VK_RIGHT,
    KeyEvent.VK_UP,
    KeyEvent.VK_DOWN,
    KeyEvent.VK_A,
    KeyEvent.VK_D,
    KeyEvent.VK_W,
    KeyEvent.VK_S,
    KeyEvent.VK_SPACE,
    KeyEvent.VK_ESCAPE,
    KeyEvent.VK_ENTER,
    KeyEvent.VK_H,
    KeyEvent.VK_M,
    KeyEvent.VK_C,
    KeyEvent.VK_F,
    KeyEvent.VK_P,
    KeyEvent.VK_R,
    KeyEvent.VK_N,
    KeyEvent.VK_1,
    KeyEvent.VK_2,
    KeyEvent.VK_3,
    KeyEvent.VK_4,
    KeyEvent.VK_5,
    KeyEvent.VK_E,
    KeyEvent.VK_Q
  )

  setupKeys()

  def setupKeys(): Unit = {
    val im = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
    val am = getActionMap()

    for (k <- controlKeys) {
      val dn = "D" + k
      val up = "U" + k

      im.put(KeyStroke.getKeyStroke(k, 0, false), dn)
      im.put(KeyStroke.getKeyStroke(k, 0, true), up)

      am.put(dn, new AbstractAction {
        override def actionPerformed(e: ActionEvent): Unit = pressKey(k)
      })

      am.put(up, new AbstractAction {
        override def actionPerformed(e: ActionEvent): Unit = keys -= k
      })
    }
  }

  def pressKey(k: Int): Unit = {
    if (state == MENU) {
      if (k == KeyEvent.VK_ENTER) hostAndPlay()
      else if (k == KeyEvent.VK_C) joinOnline()
      else if (k == KeyEvent.VK_O) offlinePlay()
      return
    }

    if (k == KeyEvent.VK_ESCAPE) {
      if (state == GAME) state = PAUSE
      else if (state == PAUSE) state = GAME
      return
    }

    if ((state == GAME_OVER || state == FINISH) && k == KeyEvent.VK_ENTER) {
      restartGame()
      return
    }

    if (k == KeyEvent.VK_C && state == GAME) {
      sendChatDialog()
      return
    }

    if (k == KeyEvent.VK_F && state == GAME) {
      toggleRace()
      return
    }

    if (k == KeyEvent.VK_P && state == GAME) {
      changeName()
      return
    }

    if (k == KeyEvent.VK_H && state == GAME) {
      headlights = !headlights
      return
    }

    if (k == KeyEvent.VK_M && state == GAME) {
      mapVisible = !mapVisible
      return
    }

    keys += k

    if (k == KeyEvent.VK_R) gear = "R"
    else if (k == KeyEvent.VK_N) gear = "N"
    else if (k == KeyEvent.VK_1) gear = "1"
    else if (k == KeyEvent.VK_2) gear = "2"
    else if (k == KeyEvent.VK_3) gear = "3"
    else if (k == KeyEvent.VK_4) gear = "4"
    else if (k == KeyEvent.VK_5) gear = "5"
  }

  // ==========================================================
  // STARTUP
  // ==========================================================
  chooseNameOnStart()

  def chooseNameOnStart(): Unit = {
    val n = JOptionPane.showInputDialog(
      this,
      "ENTER YOUR DRIVER NAME",
      "Player"
    )
    if (n != null && n.trim.nonEmpty) playerName = cleanName(n.trim)
  }

  def cleanName(n: String): String = {
    n.replace("|", " ").replace("\n", " ").take(18)
  }

  initializeTraffic()

  // ==========================================================
  // BUILT-IN SERVER
  // ==========================================================
  def startServer(): Boolean = {
    if (serverRunning) return true

    try {
      serverSocket = new ServerSocket(RealisticDriveOnline_Cockpit.PORT)
      serverRunning = true

      val t = new Thread(new Runnable {
        override def run(): Unit = acceptConnections()
      }, "DriveServerAccept")

      t.setDaemon(true)
      t.start()
      true
    } catch {
      case _: Throwable =>
        serverRunning = false
        false
    }
  }

  def acceptConnections(): Unit = {
    while (serverRunning) {
      try {
        val s = serverSocket.accept()
        s.setTcpNoDelay(true)

        val id = synchronized {
          nextServerId += 1
          nextServerId
        }

        val in = new BufferedReader(new InputStreamReader(s.getInputStream))
        val out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(s.getOutputStream)), true)

        val p = ServerPlayer(
          id,
          s,
          in,
          out,
          "Player" + id,
          0.0,
          0.0,
          0.0,
          "N",
          0,
          false,
          id % 6
        )

        serverPlayers.put(id, p)
        out.println("WELCOME|" + id)
        sendAllExistingTo(p)
        broadcastServer("JOIN|" + id + "|" + p.name, id)

        serverPool.submit(new Runnable {
          override def run(): Unit = readServerPlayer(p)
        })
      } catch {
        case _: Throwable =>
      }
    }
  }

  def sendAllExistingTo(newPlayer: ServerPlayer): Unit = {
    val it = serverPlayers.values().iterator()
    while (it.hasNext) {
      val p = it.next()
      if (p.id != newPlayer.id) sendPlayerPacket(newPlayer.output, p)
    }
  }

  def sendPlayerPacket(out: PrintWriter, p: ServerPlayer): Unit = {
    out.println(
      "P|" + p.id + "|" + p.name + "|" + p.x + "|" + p.distance + "|" +
        p.speed + "|" + p.gear + "|" + p.score + "|" + p.race + "|" + p.colorCode
    )
  }

  def readServerPlayer(p: ServerPlayer): Unit = {
    try {
      var line: String = null
      while ({ line = p.input.readLine(); line != null }) {
        val d = line.split("\\|", -1)

        if (d.nonEmpty && d(0) == "HELLO" && d.length >= 2) {
          p.name = cleanName(d(1))
          broadcastServer("NAME|" + p.id + "|" + p.name)
        }
        else if (d.nonEmpty && d(0) == "STATE" && d.length >= 10) {
          try {
            p.x = d(1).toDouble
            p.distance = d(2).toDouble
            p.speed = d(3).toDouble
            p.gear = d(4)
            p.score = d(5).toInt
            p.race = d(6).toBoolean
            p.colorCode = d(7).toInt
            broadcastServer(
              "P|" + p.id + "|" + p.name + "|" + p.x + "|" + p.distance + "|" +
                p.speed + "|" + p.gear + "|" + p.score + "|" + p.race + "|" + p.colorCode
            )
          } catch { case _: Throwable => }
        }
        else if (d.nonEmpty && d(0) == "CHAT" && d.length >= 2) {
          broadcastServer("CHAT|" + p.id + "|" + p.name + "|" + d.drop(1).mkString("|"))
        }
        else if (d.nonEmpty && d(0) == "RACE") {
          p.race = true
          broadcastServer("RACE|" + p.id + "|" + p.name)
        }
      }
    } catch { case _: Throwable => }

    serverPlayers.remove(p.id)
    broadcastServer("LEAVE|" + p.id)
    try { p.socket.close() } catch { case _: Throwable => }
  }

  def broadcastServer(msg: String, exceptId: Int = -1): Unit = {
    val it = serverPlayers.values().iterator()
    while (it.hasNext) {
      val p = it.next()
      if (p.id != exceptId) {
        try { p.output.println(msg) } catch { case _: Throwable => }
      }
    }
  }

  // ==========================================================
  // CLIENT NETWORK
  // ==========================================================
  def connectToServer(host: String): Unit = {
    disconnectClient()
    try {
      socket = new Socket()
      socket.connect(new InetSocketAddress(host, RealisticDriveOnline_Cockpit.PORT), 4000)
      socket.setTcpNoDelay(true)

      input = new BufferedReader(new InputStreamReader(socket.getInputStream))
      output = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream)), true)
      online = true
      serverHost = host

      val thread = new Thread(new Runnable {
        override def run(): Unit = readClient()
      }, "DriveClientReader")
      thread.setDaemon(true)
      thread.start()
    } catch {
      case _: Throwable =>
        online = false
    }
  }

  def readClient(): Unit = {
    try {
      var line: String = null
      while ({ line = input.readLine(); line != null }) {
        val d = line.split("\\|", -1)
        if (d.nonEmpty && d(0) == "WELCOME" && d.length >= 2) {
          try {
            playerId = d(1).toInt
            if (output != null) output.println("HELLO|" + cleanName(playerName))
          } catch { case _: Throwable => }
        }
        else if (d.nonEmpty && d(0) == "P" && d.length >= 10) {
          try {
            val id = d(1).toInt
            if (id != playerId) {
              val rp = RemotePlayer(
                cleanName(d(2)),
                d(3).toDouble,
                d(4).toDouble,
                d(5).toDouble,
                d(6),
                d(7).toInt,
                d(8).toBoolean,
                d(9).toInt
              )
              onlinePlayers.synchronized {
                onlinePlayers.update(id, rp)
              }
            }
          } catch { case _: Throwable => }
        }
        else if (d.nonEmpty && d(0) == "NAME" && d.length >= 3) {
          try {
            val id = d(1).toInt
            onlinePlayers.get(id).foreach(p => p.name = cleanName(d(2)))
          } catch { case _: Throwable => }
        }
        else if (d.nonEmpty && d(0) == "JOIN" && d.length >= 3) {
          addChat("SERVER: " + cleanName(d(2)) + " joined")
        }
        else if (d.nonEmpty && d(0) == "LEAVE" && d.length >= 2) {
          try {
            val id = d(1).toInt
            val who = onlinePlayers.get(id).map(_.name).getOrElse("Player")
            onlinePlayers.synchronized { onlinePlayers.remove(id) }
            addChat("SERVER: " + who + " left")
          } catch { case _: Throwable => }
        }
        else if (d.nonEmpty && d(0) == "CHAT" && d.length >= 4) {
          addChat(cleanName(d(2)) + ": " + d.drop(3).mkString("|"))
        }
        else if (d.nonEmpty && d(0) == "RACE" && d.length >= 3) {
          addChat("RACE REQUEST: " + cleanName(d(2)))
          raceCountdown = 3
          raceStarted = false
        }
      }
    } catch { case _: Throwable => }

    online = false
  }

  def disconnectClient(): Unit = {
    online = false
    if (socket != null) {
      try { socket.close() } catch { case _: Throwable => }
    }
    socket = null
    input = null
    output = null
  }

  def sendHello(): Unit = {
    if (online && output != null) {
      try { output.println("HELLO|" + cleanName(playerName)) } catch { case _: Throwable => }
    }
  }

  var lastStateSend = 0L

  def sendState(): Unit = {
    if (!online || output == null) return
    val now = System.currentTimeMillis()
    if (now - lastStateSend < 60) return
    lastStateSend = now
    try {
      output.println(
        "STATE|" + carX + "|" + distance + "|" + speed + "|" + gear + "|" +
          score + "|" + raceMode + "|" + (playerId % 6)
      )
    } catch { case _: Throwable => online = false }
  }

  // ==========================================================
  // ONLINE ACTIONS
  // ==========================================================
  def sendChatDialog(): Unit = {
    val text = JOptionPane.showInputDialog(
      this,
      "TYPE MESSAGE",
      "ONLINE CHAT",
      JOptionPane.PLAIN_MESSAGE
    )
    if (text != null && text.trim.nonEmpty) {
      val safe = text.replace("|", " ").replace("\n", " ").take(90)
      if (online && output != null) {
        try { output.println("CHAT|" + safe) } catch { case _: Throwable => }
      } else {
        addChat("You: " + safe)
      }
    }
  }

  def changeName(): Unit = {
    val n = JOptionPane.showInputDialog(
      this,
      "NEW DRIVER NAME",
      playerName
    )
    if (n != null && n.trim.nonEmpty) {
      playerName = cleanName(n.trim)
      sendHello()
      addChat("YOUR NAME: " + playerName)
    }
  }

  def toggleRace(): Unit = {
    raceMode = !raceMode
    if (raceMode) {
      addChat("RACE MODE: READY")
      if (online && output != null) {
        try { output.println("RACE") } catch { case _: Throwable => }
      }
    } else {
      addChat("RACE MODE: OFF")
    }
  }

  // ==========================================================
  // START MODES
  // ==========================================================
  def hostAndPlay(): Unit = {
    val started = startServer()
    if (started) connectToServer("127.0.0.1")
    resetGame()
    state = GAME
  }

  def joinOnline(): Unit = {
    val host = JOptionPane.showInputDialog(
      this,
      "ENTER HOST PC IP OR SERVER HOSTNAME",
      serverHost
    )
    if (host != null && host.trim.nonEmpty) {
      connectToServer(host.trim)
      resetGame()
      state = GAME
    }
  }

  def offlinePlay(): Unit = {
    disconnectClient()
    resetGame()
    state = GAME
  }

  def resetGame(): Unit = {
    carX = 0.0
    speed = 0.0
    rpm = 900.0
    steering = 0.0
    gear = "N"
    fuel = 100.0
    damage = 0.0
    nitro = 100.0
    distance = 0.0
    score = 0
    roadScroll = 0.0
    smoke = 0.0
    crashFlash = 0.0
    hornFlash = 0.0
    raceMode = false
    raceCountdown = 0
    raceStarted = false
    initializeTraffic()
    sendHello()
  }

  def restartGame(): Unit = {
    resetGame()
    state = GAME
  }

  // ==========================================================
  // TRAFFIC
  // ==========================================================
  def initializeTraffic(): Unit = {
    traffic.clear()
    for (i <- 0 until 20) {
      traffic += Traffic(
        rnd.nextInt(3) - 1,
        0.22 + i * 0.075 + rnd.nextDouble() * 0.10,
        0.25 + rnd.nextDouble() * 0.75,
        rnd.nextInt(3),
        randomColor()
      )
    }
  }

  def randomColor(): Color = {
    val c = Array(
      new Color(220, 45, 45),
      new Color(40, 125, 230),
      new Color(245, 205, 50),
      new Color(235, 235, 235),
      new Color(45, 48, 55),
      new Color(40, 175, 100),
      new Color(180, 70, 185),
      new Color(235, 120, 35)
    )
    c(rnd.nextInt(c.length))
  }

  // ==========================================================
  // GAME LOOP
  // ==========================================================
  val timer = new Timer(
    16,
    new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        updateGame(0.016)
        repaint()
      }
    }
  )

  timer.start()

  def updateGame(dt: Double): Unit = {
    if (state != GAME) return

    val left = keys.contains(KeyEvent.VK_LEFT) || keys.contains(KeyEvent.VK_A)
    val right = keys.contains(KeyEvent.VK_RIGHT) || keys.contains(KeyEvent.VK_D)
    val accel = keys.contains(KeyEvent.VK_UP) || keys.contains(KeyEvent.VK_W)
    val brake = keys.contains(KeyEvent.VK_DOWN) || keys.contains(KeyEvent.VK_S) || keys.contains(KeyEvent.VK_SPACE)
    val nitroPress = keys.contains(KeyEvent.VK_E)
    val horn = keys.contains(KeyEvent.VK_Q)

    // ------------------------------------------------------
    // STEERING
    // ------------------------------------------------------
    var targetSteer = 0.0
    if (left && !right) targetSteer = -1.0
    else if (right && !left) targetSteer = 1.0

    steering += (targetSteer - steering) * 0.15
    carX += steering * (5.0 + Math.abs(speed) * 0.028)

    if (carX < -315) carX = -315
    if (carX > 315) carX = 315

    // ------------------------------------------------------
    // ENGINE / GEARS
    // ------------------------------------------------------
    var ratio = 0.0
    if (gear == "1") ratio = 2.70
    else if (gear == "2") ratio = 1.95
    else if (gear == "3") ratio = 1.40
    else if (gear == "4") ratio = 1.00
    else if (gear == "5") ratio = 0.78

    if (gear == "R") {
      if (accel) speed -= 90.0 * dt
    } else if (gear == "N") {
      speed *= 0.997
    } else if (accel) {
      speed += 68.0 * ratio * dt
    }

    // ------------------------------------------------------
    // NITRO
    // ------------------------------------------------------
    if (nitroPress && nitro > 0 && speed > 25) {
      speed += 140.0 * dt
      nitro -= 30.0 * dt
    } else {
      nitro += 6.0 * dt
    }

    if (nitro < 0) nitro = 0
    if (nitro > 100) nitro = 100

    // ------------------------------------------------------
    // BRAKES
    // ------------------------------------------------------
    if (brake) {
      speed -= 140.0 * dt
      speed *= 0.973
      if (speed < 0) speed = 0
    }

    speed -= speed * 0.012 * dt
    if (speed > 250) speed = 250
    if (speed < -70) speed = -70

    // ------------------------------------------------------
    // RPM
    // ------------------------------------------------------
    if (gear == "N") {
      if (accel) rpm = 2800 else rpm = 850
    } else {
      var rMult = 19.0
      if (gear == "1") rMult = 48.0
      else if (gear == "2") rMult = 34.0
      else if (gear == "3") rMult = 27.0
      rpm = 900 + Math.abs(speed) * rMult
      if (rpm > 7000) rpm = 7000
    }

    // ------------------------------------------------------
    // FUEL
    // ------------------------------------------------------
    if (accel) fuel -= 0.008 + Math.abs(speed) * 0.000022
    if (fuel < 0) fuel = 0
    if (fuel == 0) {
      gear = "N"
      speed *= 0.99
    }

    // ------------------------------------------------------
    // WORLD / MISSION
    // ------------------------------------------------------
    roadScroll += speed * dt * 0.45
    if (speed > 0) {
      distance += speed * dt * 0.90
      score += (speed * dt * 0.085).toInt
    }

    // ------------------------------------------------------
    // TRAFFIC
    // ------------------------------------------------------
    for (t <- traffic) {
      val relative = Math.abs(speed) * 0.0021 - t.aiSpeed * 0.00115
      t.z -= relative * dt * 15.0

      if (t.z < 0.06 || t.z > 1.5) {
        t.lane = rnd.nextInt(3) - 1
        t.z = 1.10 + rnd.nextDouble() * 0.38
        t.aiSpeed = 0.25 + rnd.nextDouble() * 0.75
        t.kind = rnd.nextInt(3)
        t.color = randomColor()
      }
    }

    // ------------------------------------------------------
    // COLLISION
    // ------------------------------------------------------
    for (t <- traffic) {
      if (t.z > 0.13 && t.z < 0.29) {
        val tx = t.lane * 205.0
        if (Math.abs(carX - tx) < 76 && Math.abs(speed) > 22) {
          damage += 17 + Math.abs(speed) * 0.08
          score = Math.max(0, score - 70)
          speed *= 0.28
          smoke = 1.0
          crashFlash = 1.0
          t.z = 1.35
          if (damage >= 100) {
            damage = 100
            state = GAME_OVER
          }
        }
      }
    }

    // ------------------------------------------------------
    // EFFECTS / RACE
    // ------------------------------------------------------
    smoke *= 0.965
    crashFlash *= 0.90
    hornFlash *= 0.90
    messageTimer -= dt

    if (horn) hornFlash = 1.0

    if (raceCountdown > 0 && online) {
      raceCountdown -= 1
      if (raceCountdown <= 0) raceStarted = true
    }

    if (distance >= targetDistance) state = FINISH

    sendState()
  }

  // ==========================================================
  // PAINT
  // ==========================================================
  override def paintComponent(g0: Graphics): Unit = {
    super.paintComponent(g0)
    val g = g0.asInstanceOf[Graphics2D]
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
    g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED)

    if (state == MENU) drawMenu(g)
    else {
      drawSky(g)
      drawEnvironment(g)
      drawRoad(g)
      for (t <- traffic.sortBy(_.z).reverse) drawTraffic(g, t)
      drawOnlinePlayers(g)
      if (headlights) drawHeadlights(g)
      if (cockpit) drawPlayerCockpit(g) else drawPlayerExterior(g)
      drawTopHud(g)
      drawDashboard(g)
      if (mapVisible) drawMap(g)
      if (showChat) drawChat(g)
      if (smoke > 0.08) drawSmoke(g)
      if (crashFlash > 0.02) drawCrashFlash(g)
      if (hornFlash > 0.1) drawHorn(g)
      if (state == PAUSE) drawPause(g)
      if (state == GAME_OVER) drawGameOver(g)
      if (state == FINISH) drawFinish(g)
    }
  }

  // ==========================================================
  // MENU
  // ==========================================================
  def drawMenu(g: Graphics2D): Unit = {
    val bg = new GradientPaint(0, 0, new Color(5, 10, 18), 0, 820, new Color(18, 55, 67))
    g.setPaint(bg)
    g.fillRect(0, 0, 1280, 820)

    g.setColor(new Color(225, 232, 242))
    for (i <- 0 until 70) {
      val sx = (i * 79) % 1280
      val sy = 20 + (i * 41) % 300
      g.fillOval(sx, sy, 2, 2)
    }

    g.setColor(new Color(240, 240, 225))
    g.fillOval(1030, 50, 90, 90)

    g.setColor(new Color(0, 235, 255))
    g.setFont(new Font("Arial", Font.BOLD, 58))
    g.drawString("REALISTIC", 385, 145)

    g.setColor(new Color(255, 65, 50))
    g.drawString("DRIVE", 625, 145)

    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.PLAIN, 18))
    g.drawString("ONLINE COCKPIT MULTIPLAYER", 468, 184)

    drawButton(g, 390, 235, 500, 62, "ENTER / HOST + PLAY")
    drawButton(g, 390, 315, 500, 62, "C / JOIN ONLINE SERVER")
    drawButton(g, 390, 395, 500, 62, "O / OFFLINE DRIVE")

    g.setColor(new Color(0, 240, 170))
    g.setFont(new Font("Arial", Font.BOLD, 17))
    g.drawString("DRIVER NAME: " + playerName, 500, 515)

    g.setColor(new Color(190, 205, 215))
    g.setFont(new Font("Arial", Font.PLAIN, 13))
    g.drawString("Online players see your driver name above your car.", 455, 550)
    g.drawString("C = chat   F = race request   P = change name", 468, 578)
    g.drawString("A/D or arrows = steering   W/UP = accelerator   S/DOWN/SPACE = brake", 395, 610)
    g.drawString("R = reverse   N = neutral   1-5 = gears   E = nitro   H = lights", 435, 634)
  }

  def drawButton(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, text: String): Unit = {
    g.setColor(new Color(10, 28, 39, 235))
    g.fillRoundRect(x, y, w, h, 18, 18)
    g.setColor(new Color(0, 220, 240))
    g.setStroke(new BasicStroke(2))
    g.drawRoundRect(x, y, w, h, 18, 18)
    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.BOLD, 18))
    g.drawString(text, x + 120, y + 40)
  }

  // ==========================================================
  // SKY
  // ==========================================================
  def drawSky(g: Graphics2D): Unit = {
    val sky = new GradientPaint(0, 0, new Color(14, 28, 50), 0, 350, new Color(105, 140, 160))
    g.setPaint(sky)
    g.fillRect(0, 0, 1280, 350)

    g.setColor(new Color(240, 240, 225))
    g.fillOval(1045, 58, 68, 68)

    drawCloud(g, 100, 90, 1.0)
    drawCloud(g, 540, 120, 0.8)
    drawCloud(g, 820, 90, 1.1)
  }

  def drawCloud(g: Graphics2D, x: Int, y: Int, s: Double): Unit = {
    g.setColor(new Color(215, 225, 230, 110))
    g.fillOval(x, y, (80 * s).toInt, (35 * s).toInt)
    g.fillOval(x + (30 * s).toInt, y - (20 * s).toInt, (65 * s).toInt, (44 * s).toInt)
    g.fillOval(x + (68 * s).toInt, y, (74 * s).toInt, (35 * s).toInt)
  }

  // ==========================================================
  // ENVIRONMENT
  // ==========================================================
  def drawEnvironment(g: Graphics2D): Unit = {
    g.setColor(new Color(29, 82, 40))
    g.fillRect(0, 340, 1280, 480)

    val hills = new Polygon()
    hills.addPoint(0, 360)
    hills.addPoint(150, 300)
    hills.addPoint(300, 350)
    hills.addPoint(470, 295)
    hills.addPoint(650, 355)
    hills.addPoint(810, 298)
    hills.addPoint(1010, 350)
    hills.addPoint(1200, 300)
    hills.addPoint(1280, 375)
    hills.addPoint(1280, 440)
    hills.addPoint(0, 440)
    g.setColor(new Color(42, 75, 52))
    g.fillPolygon(hills)

    for (i <- 0 until 13) {
      drawTree(g, 25 + i * 105, 300 + (i % 3) * 12, 0.70)
      drawTree(g, 1255 - i * 105, 300 + (i % 2) * 15, 0.68)
    }
  }

  def drawTree(g: Graphics2D, x: Int, y: Int, s: Double): Unit = {
    g.setColor(new Color(90, 60, 35))
    g.fillRect(x - 6, y, 12, 58)
    g.setColor(new Color(28, 103, 42))
    g.fillOval(x - (50 * s).toInt, y - (68 * s).toInt, (100 * s).toInt, (88 * s).toInt)
    g.setColor(new Color(38, 124, 50))
    g.fillOval(x - (30 * s).toInt, y - (78 * s).toInt, (65 * s).toInt, (65 * s).toInt)
  }

  // ==========================================================
  // ROAD
  // ==========================================================
  def drawRoad(g: Graphics2D): Unit = {
    val horizon = 340
    val road = new Polygon()
    road.addPoint(500, horizon)
    road.addPoint(780, horizon)
    road.addPoint(1240, 760)
    road.addPoint(40, 760)
    g.setColor(new Color(48, 49, 53))
    g.fillPolygon(road)

    g.setColor(new Color(190, 190, 190))
    val ls = new Polygon()
    ls.addPoint(488, horizon)
    ls.addPoint(500, horizon)
    ls.addPoint(40, 760)
    ls.addPoint(0, 760)
    g.fillPolygon(ls)

    val rs = new Polygon()
    rs.addPoint(780, horizon)
    rs.addPoint(792, horizon)
    rs.addPoint(1280, 760)
    rs.addPoint(1240, 760)
    g.fillPolygon(rs)

    g.setColor(Color.WHITE)
    g.setStroke(new BasicStroke(5))
    g.drawLine(500, horizon, 40, 760)
    g.drawLine(780, horizon, 1240, 760)

    var y = horizon + 5 - (roadScroll.toInt % 92)
    while (y < 760) {
      val p = Math.max(0.0, Math.min(1.0, (y - horizon).toDouble / (760 - horizon)))
      val left = 500 - 460 * p
      val right = 780 + 460 * p
      val laneW = (right - left) / 3.0
      g.setColor(new Color(250, 220, 75))
      g.setStroke(new BasicStroke(Math.max(3.0f, (2.0 + p * 7.0).toFloat)))

      for (lane <- 1 to 2) {
        val lx = (left + laneW * lane).toInt
        g.drawLine(lx, y, lx, Math.min(760, y + 20 + (p * 85).toInt))
      }

      y += 58 + (p * 100).toInt
    }
  }

  // ==========================================================
  // TRAFFIC DRAW
  // ==========================================================
  def drawTraffic(g: Graphics2D, t: Traffic): Unit = {
    val z = Math.max(0.17, t.z)
    val scale = 1.0 / z
    val cx = (640 + t.lane * 205 * scale).toInt
    val cy = (340 + (1.20 - z) * 330).toInt
    val w = Math.max(20, (56 * scale).toInt)
    val h = Math.max(30, (108 * scale).toInt)

    g.setColor(new Color(0, 0, 0, 80))
    g.fillOval(cx - w / 2, cy + h / 2, w + 24, 14)

    g.setColor(Color.BLACK)
    g.fillRoundRect(cx - w / 2, cy - h / 2, w, h, 12, 12)

    g.setColor(t.color)
    g.fillRoundRect(cx - w / 2 + 3, cy - h / 2 + 3, w - 6, h - 6, 10, 10)

    g.setColor(new Color(55, 82, 100))
    if (t.kind == 2) {
      for (i <- 0 until 4) {
        g.fillRect(cx - w / 2 + 7, cy - h / 3 + i * Math.max(8, h / 8), w - 14, Math.max(5, h / 12))
      }
    } else {
      g.fillRoundRect(cx - w / 2 + 7, cy - h / 4, w - 14, h / 4, 6, 6)
    }

    g.setColor(new Color(255, 35, 35))
    g.fillRect(cx - w / 2 + 6, cy + h / 2 - 15, 9, 7)
    g.fillRect(cx + w / 2 - 15, cy + h / 2 - 15, 9, 7)
  }

  // ==========================================================
  // ONLINE PLAYERS DRAW
  // ==========================================================
  def drawOnlinePlayers(g: Graphics2D): Unit = {
    val players = onlinePlayers.synchronized { onlinePlayers.toArray }

    for (entry <- players) {
      val id = entry._1
      val p = entry._2
      val delta = p.distance - distance

      if (Math.abs(delta) < 850) {
        val relative = Math.max(0.13, Math.min(1.2, 0.72 - delta / 850.0))
        val scale = 1.0 / Math.max(0.18, relative)
        val cx = (640 + p.x * scale).toInt
        val cy = (340 + (1.15 - relative) * 390).toInt
        val w = Math.max(22, (64 * scale).toInt)
        val h = Math.max(35, (118 * scale).toInt)

        g.setColor(new Color(0, 240, 255, 40))
        g.fillOval(cx - w, cy - h / 2 - 12, w * 2, h + 24)

        g.setColor(Color.BLACK)
        g.fillRoundRect(cx - w / 2, cy - h / 2, w, h, 14, 14)

        g.setColor(playerColor(p.colorCode))
        g.fillRoundRect(cx - w / 2 + 3, cy - h / 2 + 3, w - 6, h - 6, 11, 11)

        g.setColor(new Color(55, 88, 110))
        g.fillRoundRect(cx - w / 2 + 8, cy - h / 4, w - 16, h / 4, 7, 7)

        g.setColor(new Color(0, 245, 255))
        g.setFont(new Font("Arial", Font.BOLD, Math.max(8, (12 * scale).toInt)))
        g.drawString(p.name, cx - w / 2, cy - h / 2 - 8)

        if (p.race) {
          g.setColor(new Color(255, 220, 50))
          g.setFont(new Font("Arial", Font.BOLD, Math.max(8, (10 * scale).toInt)))
          g.drawString("RACE", cx - w / 2, cy + h / 2 + 12)
        }
      }
    }
  }

  def playerColor(code: Int): Color = {
    val colors = Array(
      new Color(35, 160, 235),
      new Color(255, 90, 45),
      new Color(70, 210, 120),
      new Color(245, 210, 45),
      new Color(185, 80, 220),
      new Color(70, 180, 235)
    )
    colors(Math.abs(code) % colors.length)
  }

  // ==========================================================
  // EXTERIOR CAR
  // ==========================================================
  def drawPlayerExterior(g: Graphics2D): Unit = {
    val cx = (640 + carX).toInt
    val cy = 585

    g.setColor(new Color(0, 0, 0, 120))
    g.fillOval(cx - 110, cy + 75, 220, 42)

    g.setColor(new Color(15, 15, 17))
    g.fillRoundRect(cx - 88, cy - 5, 30, 95, 12, 12)
    g.fillRoundRect(cx + 58, cy - 5, 30, 95, 12, 12)

    g.setColor(new Color(205, 26, 31))
    g.fillRoundRect(cx - 82, cy - 84, 164, 190, 36, 36)

    val roof = new Polygon()
    roof.addPoint(cx - 67, cy - 72)
    roof.addPoint(cx - 43, cy - 145)
    roof.addPoint(cx + 43, cy - 145)
    roof.addPoint(cx + 67, cy - 72)
    g.setColor(new Color(27, 42, 56))
    g.fillPolygon(roof)

    val glass = new Polygon()
    glass.addPoint(cx - 51, cy - 78)
    glass.addPoint(cx - 33, cy - 132)
    glass.addPoint(cx + 33, cy - 132)
    glass.addPoint(cx + 51, cy - 78)
    g.setColor(new Color(76, 115, 138))
    g.fillPolygon(glass)

    g.setColor(Color.WHITE)
    g.fillRect(cx - 5, cy - 70, 10, 168)

    g.setColor(new Color(160, 15, 20))
    g.fillRoundRect(cx - 70, cy - 24, 140, 62, 19, 19)

    g.setColor(new Color(255, 250, 210))
    g.fillRoundRect(cx - 64, cy - 39, 35, 18, 7, 7)
    g.fillRoundRect(cx + 29, cy - 39, 35, 18, 7, 7)

    g.setColor(new Color(45, 45, 48))
    g.fillRoundRect(cx - 36, cy + 39, 72, 15, 5, 5)
  }

  // ==========================================================
  // COCKPIT VIEW
  // ==========================================================
  def drawPlayerCockpit(g: Graphics2D): Unit = {
    // lower dashboard frame
    g.setColor(new Color(12, 14, 17, 245))
    g.fillRect(0, 520, 1280, 300)

    // windshield frame
    g.setColor(new Color(20, 23, 27))
    g.fillRect(0, 0, 54, 530)
    g.fillRect(1226, 0, 54, 530)
    g.fillRect(0, 0, 1280, 28)

    // A-pillars
    g.setColor(new Color(30, 32, 35))
    g.fillPolygon(Array(0, 0, 75, 0, 170, 535, 0, 535), Array(0, 0, 535, 535), 4)
    g.fillPolygon(Array(1280, 0, 1205, 0, 1110, 535, 1280, 535), Array(0, 0, 535, 535), 4)

    // windshield reflection
    g.setColor(new Color(220, 240, 250, 18))
    val reflection = new Polygon()
    reflection.addPoint(120, 70)
    reflection.addPoint(260, 70)
    reflection.addPoint(500, 500)
    reflection.addPoint(430, 500)
    g.fillPolygon(reflection)

    // steering wheel
    val wheelX = 640
    val wheelY = 670
    g.setColor(new Color(25, 25, 27))
    g.fillOval(wheelX - 105, wheelY - 105, 210, 210)
    g.setColor(new Color(72, 72, 75))
    g.setStroke(new BasicStroke(12))
    g.drawOval(wheelX - 88, wheelY - 88, 176, 176)

    val a = steering * Math.toRadians(65)
    val spokeX = wheelX + (Math.sin(a) * 88).toInt
    val spokeY = wheelY - (Math.cos(a) * 88).toInt
    g.setColor(new Color(110, 110, 115))
    g.setStroke(new BasicStroke(7))
    g.drawLine(wheelX, wheelY, spokeX, spokeY)
    g.drawLine(wheelX, wheelY, wheelX - 70, wheelY + 45)
    g.drawLine(wheelX, wheelY, wheelX + 70, wheelY + 45)
    g.setColor(new Color(18, 18, 20))
    g.fillOval(wheelX - 18, wheelY - 18, 36, 36)

    // digital center screen
    g.setColor(new Color(5, 8, 11))
    g.fillRoundRect(505, 555, 270, 108, 18, 18)
    g.setColor(new Color(0, 235, 190))
    g.setFont(new Font("Arial", Font.BOLD, 13))
    g.drawString("DIGITAL COCKPIT", 585, 577)

    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.BOLD, 28))
    g.drawString(Math.abs(speed).toInt + "", 565, 615)
    g.setFont(new Font("Arial", Font.PLAIN, 11))
    g.drawString("KM/H", 630, 615)

    g.setColor(if (gear == "R") new Color(255, 70, 70) else new Color(0, 245, 190))
    g.setFont(new Font("Arial", Font.BOLD, 24))
    g.drawString("GEAR " + gear, 650, 648)

    // driver head + body
    g.setColor(new Color(222, 180, 148))
    g.fillOval(165, 560, 45, 45)
    g.setColor(new Color(40, 75, 130))
    g.fillRoundRect(155, 600, 65, 90, 18, 18)

    // seat belt
    g.setColor(new Color(225, 215, 170))
    g.setStroke(new BasicStroke(5))
    g.drawLine(180, 607, 216, 690)

    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.BOLD, 11))
    g.drawString(playerName, 145, 716)
    g.setFont(new Font("Arial", Font.PLAIN, 9))
    g.drawString("DRIVER / SEATBELT", 137, 732)

    // accelerator and brake pedals
    g.setColor(new Color(28, 30, 33))
    g.fillRoundRect(860, 650, 76, 115, 15, 15)
    g.fillRoundRect(950, 650, 76, 115, 15, 15)

    val accel = keys.contains(KeyEvent.VK_UP) || keys.contains(KeyEvent.VK_W)
    val brake = keys.contains(KeyEvent.VK_DOWN) || keys.contains(KeyEvent.VK_S) || keys.contains(KeyEvent.VK_SPACE)

    g.setColor(if (accel) new Color(0, 230, 160) else new Color(75, 80, 85))
    g.fillRoundRect(872, 664, 52, 88, 10, 10)

    g.setColor(if (brake) new Color(255, 55, 55) else new Color(75, 80, 85))
    g.fillRoundRect(962, 664, 52, 88, 10, 10)

    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.BOLD, 10))
    g.drawString("ACCEL", 878, 785)
    g.drawString("BRAKE", 968, 785)

    // hand / dashboard indicator strips
    drawDashboardStrip(g, 95, 548, "FUEL", fuel, new Color(0, 230, 160))
    drawDashboardStrip(g, 95, 620, "DAMAGE", damage, new Color(255, 60, 60))
    drawDashboardStrip(g, 1060, 548, "NITRO", nitro, new Color(0, 220, 255))
    drawDashboardStrip(g, 1060, 620, "RPM", rpm / 70.0, new Color(255, 170, 60))

    // small mirror
    g.setColor(new Color(18, 21, 24))
    g.fillRoundRect(565, 38, 150, 52, 18, 18)
    g.setColor(new Color(70, 95, 110))
    g.fillRoundRect(575, 48, 130, 32, 12, 12)
    g.setColor(new Color(200, 220, 230, 70))
    g.fillRect(600, 50, 25, 28)

    // cockpit controls line
    g.setColor(new Color(200, 210, 220))
    g.setFont(new Font("Arial", Font.PLAIN, 10))
    g.drawString("P NAME   C CHAT   F RACE   M MAP   H LIGHT   ESC PAUSE", 430, 806)
  }

  def drawDashboardStrip(g: Graphics2D, x: Int, y: Int, label: String, value: Double, c: Color): Unit = {
    val safe = Math.max(0.0, Math.min(100.0, value))
    g.setColor(new Color(45, 48, 52))
    g.fillRoundRect(x, y, 150, 17, 7, 7)
    g.setColor(c)
    g.fillRoundRect(x, y, (150 * safe / 100.0).toInt, 17, 7, 7)
    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.BOLD, 10))
    g.drawString(label + " " + safe.toInt + "%", x, y - 6)
  }

  // ==========================================================
  // HEADLIGHTS
  // ==========================================================
  def drawHeadlights(g: Graphics2D): Unit = {
    val left = new Polygon()
    left.addPoint(110, 500)
    left.addPoint(310, 500)
    left.addPoint(450, 340)
    left.addPoint(250, 340)

    val right = new Polygon()
    right.addPoint(970, 500)
    right.addPoint(1170, 500)
    right.addPoint(1030, 340)
    right.addPoint(830, 340)

    g.setColor(new Color(255, 247, 175, 45))
    g.fillPolygon(left)
    g.fillPolygon(right)
  }

  // ==========================================================
  // TOP HUD
  // ==========================================================
  def drawTopHud(g: Graphics2D): Unit = {
    g.setColor(new Color(5, 8, 13, 230))
    g.fillRect(0, 0, 440, 92)

    g.setColor(new Color(0, 235, 255))
    g.setFont(new Font("Arial", Font.BOLD, 18))
    g.drawString("ONLINE DRIVE", 18, 25)

    g.setColor(if (online) new Color(0, 245, 160) else new Color(255, 75, 70))
    g.setFont(new Font("Arial", Font.BOLD, 11))
    g.drawString(if (online) "? ONLINE" else "? OFFLINE", 18, 47)

    g.setColor(Color.WHITE)
    g.drawString("DRIVER: " + playerName, 95, 47)
    g.drawString("ID: " + playerId, 95, 64)

    g.setFont(new Font("Arial", Font.BOLD, 13))
    g.drawString("SPEED " + Math.abs(speed).toInt + " KM/H", 225, 25)
    g.drawString("SCORE " + score, 350, 25)
    g.drawString("PLAYERS " + onlinePlayers.synchronized { onlinePlayers.size }, 225, 47)
    g.drawString("DIST " + distance.toInt + " M", 350, 47)

    val progress = Math.min(1.0, distance / targetDistance)
    g.setColor(new Color(45, 50, 55))
    g.fillRect(225, 67, 180, 8)
    g.setColor(new Color(0, 230, 160))
    g.fillRect(225, 67, (180 * progress).toInt, 8)
  }

  // ==========================================================
  // MAP
  // ==========================================================
  def drawMap(g: Graphics2D): Unit = {
    val x = 1000
    val y = 95
    val w = 235
    val h = 185

    g.setColor(new Color(5, 9, 14, 235))
    g.fillRoundRect(x, y, w, h, 18, 18)

    g.setColor(new Color(0, 230, 255))
    g.setFont(new Font("Arial", Font.BOLD, 13))
    g.drawString("LIVE GPS / RACE", x + 15, y + 23)

    g.setColor(new Color(75, 80, 85))
    g.setStroke(new BasicStroke(23, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
    g.drawLine(x + 118, y + 45, x + 118, y + 145)

    g.setColor(new Color(0, 240, 160))
    g.setStroke(new BasicStroke(3))
    g.drawLine(x + 118, y + 45, x + 118, y + 145)

    g.setColor(new Color(255, 60, 60))
    g.fillOval(x + 110, y + 138, 16, 16)

    val progress = Math.min(1.0, distance / targetDistance)
    val py = (y + 45 + progress * 100).toInt

    g.setColor(new Color(255, 235, 50))
    g.fillOval(x + 112, py - 5, 11, 11)

    g.setColor(new Color(0, 235, 255))
    g.fillOval(x + 110, y + 90, 10, 10)

    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.PLAIN, 9))
    g.drawString("YOU", x + 138, y + 67)
    g.drawString("ONLINE: " + onlinePlayers.synchronized { onlinePlayers.size }, x + 15, y + 170)
  }

  // ==========================================================
  // CHAT
  // ==========================================================
  def drawChat(g: Graphics2D): Unit = {
    val x = 15
    val y = 115
    val w = 315
    val h = 210

    g.setColor(new Color(4, 8, 13, 190))
    g.fillRoundRect(x, y, w, h, 15, 15)

    g.setColor(new Color(0, 230, 255))
    g.setFont(new Font("Arial", Font.BOLD, 12))
    g.drawString("ONLINE CHAT  [C]", x + 12, y + 22)

    val copy = synchronized { chatLines.toArray }
    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.PLAIN, 10))

    var yy = y + 43
    for (line <- copy) {
      g.drawString(line.take(47), x + 12, yy)
      yy += 19
      if (yy > y + h - 10) yy = y + h - 10
    }
  }

  // ==========================================================
  // DASHBOARD
  // ==========================================================
  def drawDashboard(g: Graphics2D): Unit = {
    // RPM/speed gauge left/right edges, plus status
    drawGauge(g, 110, 410, 72, Math.abs(speed), 250, "SPEED")
    drawGauge(g, 1170, 410, 72, rpm / 1000.0, 7, "RPM")

    g.setColor(new Color(5, 8, 11, 240))
    g.fillRoundRect(470, 704, 340, 68, 20, 20)

    g.setColor(new Color(0, 230, 190))
    g.setFont(new Font("Arial", Font.BOLD, 12))
    g.drawString("TRANSMISSION", 580, 725)

    g.setColor(if (gear == "R") new Color(255, 60, 60) else new Color(0, 245, 190))
    g.setFont(new Font("Arial", Font.BOLD, 35))
    g.drawString(gear, 625, 758)

    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.BOLD, 10))
    g.drawString("R REVERSE   N NEUTRAL   1-5 DRIVE", 535, 788)

    // race badge
    if (raceMode) {
      g.setColor(new Color(255, 210, 50))
      g.fillRoundRect(510, 95, 260, 35, 12, 12)
      g.setColor(Color.BLACK)
      g.setFont(new Font("Arial", Font.BOLD, 14))
      g.drawString(if (raceStarted) "RACE LIVE" else "RACE READY", 585, 118)
    }

    // name panel
    g.setColor(new Color(5, 8, 11, 220))
    g.fillRoundRect(1040, 305, 195, 60, 12, 12)
    g.setColor(new Color(0, 235, 255))
    g.setFont(new Font("Arial", Font.BOLD, 10))
    g.drawString("DRIVER", 1055, 323)
    g.setColor(Color.WHITE)
    g.drawString(playerName, 1055, 344)
    g.drawString("ONLINE " + onlinePlayers.synchronized { onlinePlayers.size }, 1055, 359)
  }

  def drawGauge(g: Graphics2D, cx: Int, cy: Int, r: Int, value: Double, max: Double, title: String): Unit = {
    g.setColor(new Color(5, 7, 9, 235))
    g.fillOval(cx - r, cy - r, 2 * r, 2 * r)
    g.setColor(new Color(105, 110, 115))
    g.setStroke(new BasicStroke(3))
    g.drawOval(cx - r, cy - r, 2 * r, 2 * r)

    for (i <- 0 to 10) {
      val a = Math.toRadians(-135 + i * 27)
      val x1 = cx + (Math.cos(a) * (r - 7)).toInt
      val y1 = cy + (Math.sin(a) * (r - 7)).toInt
      val x2 = cx + (Math.cos(a) * (r - 16)).toInt
      val y2 = cy + (Math.sin(a) * (r - 16)).toInt
      g.setColor(Color.WHITE)
      g.drawLine(x1, y1, x2, y2)
    }

    val safe = Math.max(0.0, Math.min(max, value))
    val a = Math.toRadians(-135 + safe / max * 270.0)
    val px = cx + (Math.cos(a) * (r - 22)).toInt
    val py = cy + (Math.sin(a) * (r - 22)).toInt

    g.setColor(new Color(255, 80, 55))
    g.setStroke(new BasicStroke(4))
    g.drawLine(cx, cy, px, py)
    g.setColor(Color.WHITE)
    g.fillOval(cx - 4, cy - 4, 8, 8)

    g.setFont(new Font("Arial", Font.BOLD, 10))
    g.drawString(title, cx - 23, cy + 40)
  }

  // ==========================================================
  // PAUSE / END
  // ==========================================================
  def drawPause(g: Graphics2D): Unit = {
    g.setColor(new Color(0, 0, 0, 175))
    g.fillRect(0, 0, 1280, 820)
    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.BOLD, 56))
    g.drawString("PAUSED", 520, 325)
    g.setFont(new Font("Arial", Font.PLAIN, 20))
    g.drawString("PRESS ESC TO CONTINUE", 480, 370)
  }

  def drawGameOver(g: Graphics2D): Unit = {
    g.setColor(new Color(0, 0, 0, 185))
    g.fillRect(0, 0, 1280, 820)
    g.setColor(new Color(255, 45, 45))
    g.setFont(new Font("Arial", Font.BOLD, 52))
    g.drawString("VEHICLE DESTROYED", 395, 310)
    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.BOLD, 21))
    g.drawString("SCORE : " + score, 535, 365)
    g.drawString("DISTANCE : " + distance.toInt + " M", 500, 400)
    g.setColor(new Color(0, 240, 180))
    g.drawString("PRESS ENTER TO RESTART", 465, 450)
  }

  def drawFinish(g: Graphics2D): Unit = {
    g.setColor(new Color(0, 25, 17, 185))
    g.fillRect(0, 0, 1280, 820)
    g.setColor(new Color(0, 255, 175))
    g.setFont(new Font("Arial", Font.BOLD, 50))
    g.drawString("MISSION COMPLETE!", 425, 310)
    g.setColor(Color.WHITE)
    g.setFont(new Font("Arial", Font.BOLD, 22))
    g.drawString("FINAL SCORE : " + score, 510, 365)
    g.setColor(new Color(0, 240, 180))
    g.drawString("PRESS ENTER FOR NEW DRIVE", 450, 440)
  }

  // ==========================================================
  // EFFECTS
  // ==========================================================
  def drawSmoke(g: Graphics2D): Unit = {
    val cx = (640 + carX).toInt
    for (i <- 0 until 7) {
      val alpha = Math.max(15, 95 - i * 11)
      g.setColor(new Color(210, 210, 210, alpha))
      val s = 20 + i * 7
      g.fillOval(cx - 40 + rnd.nextInt(60), 620 - i * 13, s, s)
    }
  }

  def drawCrashFlash(g: Graphics2D): Unit = {
    g.setColor(new Color(255, 50, 30, (crashFlash * 120).toInt))
    g.fillRect(0, 0, 1280, 650)
  }

  def drawHorn(g: Graphics2D): Unit = {
    g.setColor(new Color(255, 230, 80, (hornFlash * 120).toInt))
    g.setFont(new Font("Arial", Font.BOLD, 22))
    g.drawString("HORN", 610, 505)
  }
}