Code Sketch


yoiiii
By: Mhalsakant School
Category: Programming
import javax.swing.JFrame
import javax.swing.JPanel
import javax.swing.JButton
import javax.swing.JLabel
import javax.swing.JTextField
import javax.swing.JTextArea
import javax.swing.JScrollPane
import javax.swing.JOptionPane
import javax.swing.WindowConstants
import javax.swing.SwingUtilities
import javax.swing.BoxLayout
import javax.swing.BorderFactory

import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Color
import java.awt.Font
import java.awt.BasicStroke
import java.awt.GradientPaint
import java.awt.Dimension
import java.awt.RenderingHints

import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.ActionListener

import java.net.ServerSocket
import java.net.Socket
import java.net.InetAddress

import java.io.PrintWriter
import java.io.BufferedReader
import java.io.InputStreamReader

import javax.sound.sampled.AudioFormat
import javax.sound.sampled.AudioSystem

import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.Set
import scala.util.Try


/* =========================================================
   REAL DRIVE X
   ========================================================= */

var frame: JFrame = null
var root: JPanel = null
var cards: java.awt.CardLayout = null

var menuPanel: JPanel = null
var lobbyPanel: JPanel = null
var gamePanel: DrivePanel = null

var lobbyNameField: JTextField = null
var lobbyHostField: JTextField = null
var lobbyPortField: JTextField = null
var lobbyChatField: JTextField = null

var lobbyStatusLabel: JLabel = null
var lobbyPlayersLabel: JLabel = null
var lobbyChatArea: JTextArea = null
var lobbyModeLabel: JLabel = null

var currentScreen = "menu"

var online = false
var multiplayer = false
var soloOnline = false
var ready = false
var raceStarted = false

var playerName = "Driver"

var lastHost = "127.0.0.1"
var lastPort = 5555

var serverSocket: ServerSocket = null
var serverRunning = false
var actualServerPort = 5555

var clientSocket: Socket = null
var clientOut: PrintWriter = null
var clientIn: BufferedReader = null

var clientId = "P0"
var clientConnected = false


/* =========================================================
   DATA
   ========================================================= */

case class ServerClient(
  id: String,
  name: String,
  socket: Socket,
  out: PrintWriter,
  var x: Double,
  var distance: Double,
  var speed: Double,
  var gear: String,
  var mode: String,
  var ready: Boolean
)

case class RemotePlayer(
  id: String,
  name: String,
  var x: Double,
  var distance: Double,
  var speed: Double,
  var gear: String,
  var mode: String,
  var ready: Boolean
)

case class TrafficCar(
  var lane: Int,
  var z: Double,
  var speed: Double,
  var kind: String,
  var police: Boolean
)

val serverClients =
  ArrayBuffer[ServerClient]()

val remotePlayers =
  ArrayBuffer[RemotePlayer]()

val traffic =
  ArrayBuffer[TrafficCar]()

val keys =
  Set[Int]()


/* =========================================================
   GAME VARIABLES
   ========================================================= */

var gameDistance = 0.0
var gameSpeed = 0.0

var fuel = 100.0
var damage = 0.0
var score = 0

var missionNumber = 1
var missionProgress = 0
var missionTarget = 1000.0

var carX = 0.0
var steering = 0.0

var throttle = false
var braking = false
var nitro = false
var horn = false

var gear = "N"
var autoGear = true

var lights = false
var highBeam = false
var leftIndicator = false
var rightIndicator = false
var hazards = false

var rain = false
var wipers = true

var paused = false
var gameRunning = false

var timePhase = 0

var flashMessage = ""
var flashTime = 0L

var lastTick = System.currentTimeMillis()
var lastNetworkSend = 0L
var lastTrafficSpawn = 0L
var lastSound = 0L


/* =========================================================
   GLOBAL ROAD HELPERS
   IMPORTANT: laneToX is GLOBAL
   ========================================================= */

def laneToX(lane: Int): Double = {

  if (lane == 0) {
    -0.66
  } else if (lane == 1) {
    0.0
  } else {
    0.66
  }
}

def laneCenterX(
  width: Int,
  lane: Int,
  roadHalf: Double
): Double = {

  width / 2.0 +
  laneToX(lane) * roadHalf
}


/* =========================================================
   GENERAL HELPERS
   ========================================================= */

def showCard(name: String): Unit = {

  currentScreen = name

  cards.show(
    root,
    name
  )
}

def cleanText(s: String): String = {

  if (s == null) {
    ""
  } else {
    s.replace("|", "/")
      .replace("~", "-")
  }
}

def boolText(v: Boolean): String = {

  if (v) {
    "ON"
  } else {
    "OFF"
  }
}

def modeText(): String = {

  if (!online) {
    "OFFLINE SOLO"
  } else if (multiplayer) {
    "MULTIPLAYER"
  } else {
    "ONLINE SOLO"
  }
}

def gearNumber(g: String): Int = {

  if (g == "R") {
    -1
  } else if (g == "N") {
    0
  } else {
    Try(g.toInt).getOrElse(0)
  }
}

def addFlash(text: String): Unit = {

  flashMessage = text

  flashTime =
    System.currentTimeMillis() + 2200L

  playTone(
    700,
    70
  )
}

def localIP(): String = {

  try {

    InetAddress
      .getLocalHost
      .getHostAddress

  } catch {

    case _: Throwable =>
      "127.0.0.1"
  }
}


/* =========================================================
   SOUND
   ========================================================= */

def playTone(
  frequency: Int,
  duration: Int
): Unit = {

  val now =
    System.currentTimeMillis()

  if (now - lastSound < 35L) {
    return
  }

  lastSound = now

  val soundThread =
    new Thread(
      new Runnable {

        def run(): Unit = {

          try {

            val sampleRate =
              44100.0f

            val format =
              new AudioFormat(
                sampleRate,
                8,
                1,
                true,
                false
              )

            val line =
              AudioSystem.getSourceDataLine(
                format
              )

            line.open(format)
            line.start()

            val count =
              Math.max(
                1,
                (
                  sampleRate *
                  duration.toDouble /
                  1000.0
                ).toInt
              )

            val buffer =
              new Array[Byte](count)

            var i = 0

            while (i < count) {

              val angle =
                2.0 *
                Math.PI *
                frequency.toDouble *
                i.toDouble /
                sampleRate.toDouble

              buffer(i) =
                (
                  Math.sin(angle) *
                  105.0
                ).toByte

              i += 1
            }

            line.write(
              buffer,
              0,
              buffer.length
            )

            line.drain()
            line.stop()
            line.close()

          } catch {

            case _: Throwable =>

              try {
                java.awt.Toolkit
                  .getDefaultToolkit
                  .beep()
              } catch {
                case _: Throwable =>
              }
          }
        }
      }
    )

  soundThread.setDaemon(true)
  soundThread.start()
}


/* =========================================================
   GEAR / CAR CONTROLS
   ========================================================= */

def toggleLights(): Unit = {

  lights = !lights

  if (lights) {
    addFlash("HEADLIGHTS ON")
  } else {
    addFlash("HEADLIGHTS OFF")
  }
}

def toggleHighBeam(): Unit = {

  highBeam = !highBeam

  if (highBeam) {
    addFlash("HIGH BEAM ON")
  } else {
    addFlash("HIGH BEAM OFF")
  }
}

def toggleAutoGear(): Unit = {

  autoGear = !autoGear

  if (autoGear) {
    addFlash("AUTO GEAR")
  } else {
    addFlash("MANUAL GEAR")
  }
}

def toggleRain(): Unit = {

  rain = !rain

  if (rain) {
    addFlash("RAIN STARTED")
    playTone(240, 120)
  } else {
    addFlash("RAIN STOPPED")
  }
}

def shiftUp(): Unit = {

  if (autoGear) {
    return
  }

  if (gear == "N") {
    gear = "1"
  } else if (gear == "1") {
    gear = "2"
  } else if (gear == "2") {
    gear = "3"
  } else if (gear == "3") {
    gear = "4"
  } else if (gear == "4") {
    gear = "5"
  }

  playTone(430, 55)
}

def manualReverse(): Unit = {

  if (!autoGear) {
    gear = "R"
    playTone(300, 70)
  }
}


/* =========================================================
   SERVER
   ========================================================= */

def startServer(
  requestedPort: Int
): Boolean = {

  if (serverRunning) {
    return true
  }

  var port = requestedPort
  var opened: ServerSocket = null

  while (
    port < requestedPort + 25 &&
    opened == null
  ) {

    try {

      val temp =
        new ServerSocket()

      temp.setReuseAddress(true)

      temp.bind(
        new java.net.InetSocketAddress(
          "0.0.0.0",
          port
        )
      )

      opened = temp

    } catch {

      case _: Throwable =>
        port += 1
    }
  }

  if (opened == null) {
    return false
  }

  serverSocket = opened
  actualServerPort = port
  serverRunning = true

  addLobbyChat(
    "[SERVER] Listening on " +
    localIP() +
    ":" +
    actualServerPort
  )

  val acceptThread =
    new Thread(
      new Runnable {

        def run(): Unit = {

          while (serverRunning) {

            try {

              val socket =
                serverSocket.accept()

              val clientThread =
                new Thread(
                  new Runnable {

                    def run(): Unit = {

                      handleServerClient(
                        socket
                      )
                    }
                  }
                )

              clientThread.setDaemon(true)
              clientThread.start()

            } catch {

              case _: Throwable =>
            }
          }
        }
      }
    )

  acceptThread.setDaemon(true)
  acceptThread.start()

  true
}

def broadcastServer(
  message: String
): Unit = {

  serverClients.synchronized {

    var i = 0

    while (i < serverClients.length) {

      try {

        serverClients(i)
          .out
          .println(message)

        serverClients(i)
          .out
          .flush()

      } catch {

        case _: Throwable =>
      }

      i += 1
    }
  }
}

def broadcastPlayers(): Unit = {

  val builder =
    new StringBuilder(
      "PLAYERS"
    )

  serverClients.synchronized {

    var i = 0

    while (
      i < serverClients.length
    ) {

      val c =
        serverClients(i)

      builder.append("|")

      builder.append(
        cleanText(c.id)
      )

      builder.append("~")

      builder.append(
        cleanText(c.name)
      )

      builder.append("~")

      builder.append(c.x)

      builder.append("~")

      builder.append(c.distance)

      builder.append("~")

      builder.append(c.speed)

      builder.append("~")

      builder.append(
        cleanText(c.gear)
      )

      builder.append("~")

      builder.append(
        cleanText(c.mode)
      )

      builder.append("~")

      builder.append(c.ready)

      i += 1
    }
  }

  broadcastServer(
    builder.toString()
  )

  updateLobbyPlayers()
}

def removeServerClient(
  id: String
): Unit = {

  serverClients.synchronized {

    var i =
      serverClients.length - 1

    while (i >= 0) {

      if (
        serverClients(i).id == id
      ) {

        try {
          serverClients(i)
            .socket
            .close()
        } catch {
          case _: Throwable =>
        }

        serverClients.remove(i)
      }

      i -= 1
    }
  }

  broadcastPlayers()
}

def handleServerClient(
  socket: Socket
): Unit = {

  var input: BufferedReader = null
  var output: PrintWriter = null
  var player: ServerClient = null

  try {

    input =
      new BufferedReader(
        new InputStreamReader(
          socket.getInputStream
        )
      )

    output =
      new PrintWriter(
        socket.getOutputStream,
        true
      )

    val first =
      input.readLine()

    if (first == null) {
      socket.close()
      return
    }

    val h =
      first.split("\\|", -1)

    var name = "Driver"

    if (h.length >= 2) {
      name =
        cleanText(h(1))
    }

    val id =
      "P" +
      Math.abs(
        System.nanoTime()
      ).toString
        .takeRight(6)

    player =
      ServerClient(
        id,
        name,
        socket,
        output,
        0.0,
        0.0,
        0.0,
        "N",
        "ONLINE SOLO",
        false
      )

    serverClients.synchronized {
      serverClients += player
    }

    output.println(
      "WELCOME|" +
      id +
      "|" +
      actualServerPort
    )

    output.flush()

    broadcastServer(
      "SYSTEM|" +
      cleanText(name) +
      " joined."
    )

    broadcastPlayers()

    var listening = true

    while (listening) {

      val line =
        input.readLine()

      if (line == null) {

        listening = false

      } else {

        val p =
          line.split("\\|", -1)

        if (p.length > 0) {

          if (p(0) == "STATE") {

            if (p.length >= 9) {

              player.x =
                Try(p(3).toDouble)
                  .getOrElse(0.0)

              player.distance =
                Try(p(4).toDouble)
                  .getOrElse(0.0)

              player.speed =
                Try(p(5).toDouble)
                  .getOrElse(0.0)

              player.gear = p(6)
              player.mode = p(7)
              player.ready =
                p(8) == "true"
            }

            broadcastPlayers()
          }

          if (p(0) == "CHAT") {

            if (p.length >= 3) {

              broadcastServer(
                "CHAT|" +
                cleanText(player.name) +
                "|" +
                cleanText(p(2))
              )
            }
          }

          if (p(0) == "READY") {

            player.ready = true

            broadcastServer(
              "SYSTEM|" +
              cleanText(player.name) +
              " is READY."
            )

            broadcastPlayers()
          }

          if (p(0) == "NOTREADY") {

            player.ready = false
            broadcastPlayers()
          }

          if (p(0) == "MODE") {

            if (p.length >= 2) {
              player.mode = p(1)
            }

            broadcastPlayers()
          }

          if (p(0) == "START") {

            broadcastServer(
              "RACE_START"
            )
          }

          if (p(0) == "LOBBY") {

            player.mode = "LOBBY"
            player.ready = false

            broadcastPlayers()
          }
        }
      }
    }

  } catch {

    case _: Throwable =>
  }

  if (player != null) {

    broadcastServer(
      "SYSTEM|" +
      cleanText(player.name) +
      " left."
    )

    removeServerClient(
      player.id
    )

  } else {

    try {
      socket.close()
    } catch {
      case _: Throwable =>
    }
  }
}

def stopServer(): Unit = {

  serverRunning = false

  try {
    if (serverSocket != null) {
      serverSocket.close()
    }
  } catch {
    case _: Throwable =>
  }

  serverClients.synchronized {

    var i = 0

    while (
      i < serverClients.length
    ) {

      try {
        serverClients(i)
          .socket
          .close()
      } catch {
        case _: Throwable =>
      }

      i += 1
    }

    serverClients.clear()
  }
}


/* =========================================================
   CLIENT NETWORK
   ========================================================= */

def disconnectClient(): Unit = {

  clientConnected = false
  online = false
  ready = false
  raceStarted = false

  try {
    if (clientSocket != null) {
      clientSocket.close()
    }
  } catch {
    case _: Throwable =>
  }

  clientSocket = null
  clientOut = null
  clientIn = null

  remotePlayers.synchronized {
    remotePlayers.clear()
  }
}

def connectToServer(
  host: String,
  port: Int
): Boolean = {

  disconnectClient()

  try {

    val socket =
      new Socket()

    socket.connect(
      new java.net.InetSocketAddress(
        host,
        port
      ),
      3500
    )

    socket.setTcpNoDelay(true)

    clientSocket = socket

    clientOut =
      new PrintWriter(
        socket.getOutputStream,
        true
      )

    clientIn =
      new BufferedReader(
        new InputStreamReader(
          socket.getInputStream
        )
      )

    clientOut.println(
      "HELLO|" +
      cleanText(playerName)
    )

    clientOut.flush()

    clientConnected = true
    online = true

    lastHost = host
    lastPort = port

    val listener =
      new Thread(
        new Runnable {

          def run(): Unit = {

            listenClient()
          }
        }
      )

    listener.setDaemon(true)
    listener.start()

    true

  } catch {

    case _: Throwable =>

      clientConnected = false
      online = false

      false
  }
}

def sendClient(
  message: String
): Unit = {

  if (!clientConnected) {
    return
  }

  try {

    clientOut.println(message)
    clientOut.flush()

  } catch {

    case _: Throwable =>
      clientConnected = false
      online = false
  }
}

def listenClient(): Unit = {

  try {

    var running = true

    while (
      running &&
      clientConnected
    ) {

      val line =
        clientIn.readLine()

      if (line == null) {
        running = false
      } else {
        receiveMessage(line)
      }
    }

  } catch {

    case _: Throwable =>
  }

  SwingUtilities.invokeLater(
    new Runnable {

      def run(): Unit = {

        if (
          clientConnected
        ) {

          clientConnected = false
          online = false

          if (
            lobbyStatusLabel != null
          ) {

            lobbyStatusLabel.setText(
              "CONNECTION CLOSED"
            )
          }
        }
      }
    }
  )
}

def receiveMessage(
  message: String
): Unit = {

  val p =
    message.split("\\|", -1)

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

  if (p(0) == "WELCOME") {

    if (p.length >= 2) {
      clientId = p(1)
    }

    SwingUtilities.invokeLater(
      new Runnable {

        def run(): Unit = {

          if (
            lobbyStatusLabel != null
          ) {

            lobbyStatusLabel.setText(
              "CONNECTED ? ID " +
              clientId
            )
          }
        }
      }
    )
  }

  if (p(0) == "SYSTEM") {

    if (p.length >= 2) {

      addLobbyChat(
        "[SYSTEM] " +
        p(1)
      )
    }
  }

  if (p(0) == "CHAT") {

    if (p.length >= 3) {

      addLobbyChat(
        p(1) +
        ": " +
        p(2)
      )
    }
  }

  if (p(0) == "PLAYERS") {

    parsePlayers(p)
  }

  if (p(0) == "RACE_START") {

    raceStarted = true

    startGame()
  }
}

def parsePlayers(
  p: Array[String]
): Unit = {

  remotePlayers.synchronized {

    remotePlayers.clear()

    var i = 1

    while (
      i < p.length
    ) {

      val q =
        p(i).split(
          "~",
          -1
        )

      if (q.length >= 8) {

        if (q(0) != clientId) {

          remotePlayers +=
            RemotePlayer(
              q(0),
              q(1),
              Try(q(2).toDouble)
                .getOrElse(0.0),
              Try(q(3).toDouble)
                .getOrElse(0.0),
              Try(q(4).toDouble)
                .getOrElse(0.0),
              q(5),
              q(6),
              q(7) == "true"
            )
        }
      }

      i += 1
    }
  }

  updateLobbyPlayers()
}

def sendGameState(): Unit = {

  if (
    !online ||
    !clientConnected
  ) {
    return
  }

  val now =
    System.currentTimeMillis()

  if (
    now - lastNetworkSend < 80L
  ) {
    return
  }

  lastNetworkSend = now

  sendClient(
    "STATE|" +
    clientId +
    "|" +
    playerName +
    "|" +
    carX +
    "|" +
    gameDistance +
    "|" +
    gameSpeed +
    "|" +
    gear +
    "|" +
    modeText() +
    "|" +
    ready
  )
}

def sendChat(
  text: String
): Unit = {

  if (
    text.trim.isEmpty
  ) {
    return
  }

  if (clientConnected) {

    sendClient(
      "CHAT|" +
      clientId +
      "|" +
      cleanText(text)
    )

  } else {

    addLobbyChat(
      "LOCAL: " +
      text
    )
  }
}


/* =========================================================
   LOBBY UPDATE
   ========================================================= */

def addLobbyChat(
  text: String
): Unit = {

  if (
    lobbyChatArea == null
  ) {
    return
  }

  SwingUtilities.invokeLater(
    new Runnable {

      def run(): Unit = {

        lobbyChatArea.append(
          text +
          "\n"
        )

        lobbyChatArea.setCaretPosition(
          lobbyChatArea
            .getDocument
            .getLength
        )
      }
    }
  )
}

def updateLobbyPlayers(): Unit = {

  if (
    lobbyPlayersLabel == null
  ) {
    return
  }

  val remoteCount =
    remotePlayers.synchronized {
      remotePlayers.length
    }

  val hostCount =
    serverClients.synchronized {
      serverClients.length
    }

  SwingUtilities.invokeLater(
    new Runnable {

      def run(): Unit = {

        if (serverRunning) {

          lobbyPlayersLabel.setText(
            "HOST: " +
            localIP() +
            ":" +
            actualServerPort +
            "    PLAYERS: " +
            hostCount
          )

        } else {

          lobbyPlayersLabel.setText(
            "ONLINE PLAYERS: " +
            remoteCount
          )
        }
      }
    }
  )
}


/* =========================================================
   GAME
   ========================================================= */

def resetGame(): Unit = {

  gameDistance = 0.0
  gameSpeed = 0.0
  fuel = 100.0
  damage = 0.0
  score = 0

  missionNumber = 1
  missionProgress = 0
  missionTarget = 1000.0

  carX = 0.0
  steering = 0.0

  throttle = false
  braking = false
  nitro = false
  horn = false

  gear = "N"

  lights = false
  highBeam = false

  paused = false
  gameRunning = true

  traffic.clear()

  var i = 0

  while (i < 12) {

    traffic +=
      TrafficCar(
        i % 3,
        0.15 + i.toDouble * 0.09,
        35.0 +
        (i % 5) * 10.0,
        getTrafficKind(i),
        i % 8 == 0
      )

    i += 1
  }

  playTone(
    820,
    120
  )
}

def getTrafficKind(
  i: Int
): String = {

  if (i % 5 == 0) {
    "TRUCK"
  } else if (i % 3 == 0) {
    "BUS"
  } else {
    "CAR"
  }
}

def startGame(): Unit = {

  resetGame()

  showCard("game")

  if (
    gamePanel != null
  ) {
    gamePanel.requestFocusInWindow()
  }
}

def updateGame(): Unit = {

  if (
    !gameRunning ||
    paused
  ) {
    return
  }

  val now =
    System.currentTimeMillis()

  val dtRaw =
    (
      now - lastTick
    ).toDouble / 1000.0

  val dt =
    Math.min(
      0.06,
      dtRaw
    )

  lastTick = now

  updateControls(dt)
  updateTraffic(dt)
  detectTrafficCollision()
  updateMission()

  timePhase =
    (
      gameDistance /
      2500.0
    ).toInt % 4

  sendGameState()
}

def updateControls(
  dt: Double
): Unit = {

  val left =
    keys.contains(KeyEvent.VK_A) ||
    keys.contains(KeyEvent.VK_LEFT)

  val right =
    keys.contains(KeyEvent.VK_D) ||
    keys.contains(KeyEvent.VK_RIGHT)

  val up =
    keys.contains(KeyEvent.VK_W) ||
    keys.contains(KeyEvent.VK_UP)

  val down =
    keys.contains(KeyEvent.VK_S) ||
    keys.contains(KeyEvent.VK_DOWN)

  if (left) {

    steering -=
      2.7 * dt

  } else if (right) {

    steering +=
      2.7 * dt

  } else {

    if (steering > 0.0) {
      steering -= 1.5 * dt
    }

    if (steering < 0.0) {
      steering += 1.5 * dt
    }
  }

  if (steering > 1.0) {
    steering = 1.0
  }

  if (steering < -1.0) {
    steering = -1.0
  }

  val gas =
    throttle || up

  val brake =
    braking || down

  var acceleration = 0.0

  val currentGear =
    gearNumber(gear)

  if (gas) {

    if (currentGear > 0) {

      acceleration =
        30.0 +
        currentGear *
        7.0
    }

    if (gear == "R") {
      acceleration = -25.0
    }
  }

  if (brake) {
    acceleration -= 65.0
  }

  if (
    nitro &&
    fuel > 2.0 &&
    gameSpeed > 10.0
  ) {

    acceleration += 75.0
    fuel -= 6.0 * dt
  }

  gameSpeed +=
    acceleration * dt

  if (!gas && !brake) {

    if (gameSpeed > 0.0) {
      gameSpeed -= 8.0 * dt
    }

    if (gameSpeed < 0.0) {
      gameSpeed += 8.0 * dt
    }
  }

  if (gameSpeed > 180.0) {
    gameSpeed = 180.0
  }

  if (gameSpeed < -40.0) {
    gameSpeed = -40.0
  }

  if (autoGear) {
    autoShift()
  }

  carX +=
    steering *
    (
      0.08 +
      Math.abs(gameSpeed) *
      0.0012
    ) *
    dt

  if (carX < -1.0) {
    carX = -1.0
  }

  if (carX > 1.0) {
    carX = 1.0
  }

  val forwardSpeed =
    Math.max(
      0.0,
      gameSpeed
    )

  gameDistance +=
    forwardSpeed *
    dt *
    0.12

  fuel -=
    (
      0.5 +
      forwardSpeed *
      0.0035
    ) *
    dt

  if (fuel < 0.0) {
    fuel = 0.0
  }

  if (fuel <= 0.0) {
    gameSpeed -=
      15.0 * dt
  }

  score +=
    (
      forwardSpeed *
      dt *
      0.12
    ).toInt
}

def autoShift(): Unit = {

  if (gameSpeed < 4.0) {

    gear = "N"

  } else if (gameSpeed < 28.0) {

    gear = "1"

  } else if (gameSpeed < 55.0) {

    gear = "2"

  } else if (gameSpeed < 82.0) {

    gear = "3"

  } else if (gameSpeed < 115.0) {

    gear = "4"

  } else {

    gear = "5"
  }
}


/* =========================================================
   TRAFFIC
   ========================================================= */

def updateTraffic(
  dt: Double
): Unit = {

  var i = 0

  while (i < traffic.length) {

    val car =
      traffic(i)

    car.z +=
      (
        car.speed -
        gameSpeed
      ) *
      dt *
      0.0015

    if (car.z > 1.25) {

      car.z = -0.15

      car.lane =
        Math.abs(
          (
            System.nanoTime() /
            1000L
          ).toInt
        ) % 3
    }

    if (car.z < -0.4) {
      car.z = 1.05
    }

    i += 1
  }

  val now =
    System.currentTimeMillis()

  if (
    now - lastTrafficSpawn >
    2300L
  ) {

    lastTrafficSpawn = now

    val lane =
      Math.abs(
        (
          System.nanoTime() /
          1000L
        ).toInt
      ) % 3

    traffic +=
      TrafficCar(
        lane,
        -0.15,
        40.0 +
        (
          Math.abs(
            (
              System.nanoTime() /
              1000000L
            ) % 80L
          ).toDouble
        ),
        "CAR",
        false
      )

    if (traffic.length > 20) {
      traffic.remove(0)
    }
  }
}


/* =========================================================
   COLLISION
   ========================================================= */

def detectTrafficCollision(): Unit = {

  var i = 0

  while (i < traffic.length) {

    val car =
      traffic(i)

    if (
      car.z > 0.82 &&
      car.z < 1.02
    ) {

      val trafficX =
        laneToX(
          car.lane
        )

      val difference =
        Math.abs(
          trafficX -
          carX
        )

      if (difference < 0.22) {

        val impact =
          Math.abs(
            gameSpeed -
            car.speed
          )

        damage +=
          4.0 +
          impact *
          0.07

        score -= 25

        if (score < 0) {
          score = 0
        }

        car.z = -0.25

        playTone(
          115,
          170
        )

        if (car.police) {

          score -= 75

          if (score < 0) {
            score = 0
          }

          addFlash(
            "POLICE FINE!"
          )

        } else {

          addFlash(
            "COLLISION!"
          )
        }
      }
    }

    i += 1
  }

  if (damage > 100.0) {
    damage = 100.0
    gameSpeed *= 0.55
    addFlash("VEHICLE DAMAGED")
  }
}


/* =========================================================
   MISSION
   ========================================================= */

def updateMission(): Unit = {

  missionProgress =
    Math.min(
      missionTarget.toInt,
      gameDistance.toInt
    )

  if (
    gameDistance >=
    missionTarget
  ) {

    missionNumber += 1

    missionTarget +=
      1000.0 +
      missionNumber * 300.0

    score += 1000

    playTone(
      1000,
      150
    )

    addFlash(
      "CHECKPOINT +" +
      missionNumber
    )
  }
}


/* =========================================================
   NAVIGATION
   ========================================================= */

def startMultiplayer(): Unit = {

  if (!clientConnected) {

    addFlash(
      "NOT CONNECTED"
    )

    return
  }

  multiplayer = true
  soloOnline = false
  ready = true

  sendClient(
    "MODE|MULTIPLAYER"
  )

  sendClient(
    "READY"
  )

  sendClient(
    "START"
  )

  startGame()
}

def startOnlineSolo(): Unit = {

  if (!clientConnected) {

    addFlash(
      "NOT CONNECTED"
    )

    return
  }

  multiplayer = false
  soloOnline = true
  ready = true

  sendClient(
    "MODE|ONLINE SOLO"
  )

  sendClient(
    "READY"
  )

  startGame()
}

def leaveToLobby(): Unit = {

  gameRunning = false

  throttle = false
  braking = false
  nitro = false
  horn = false

  if (clientConnected) {

    sendClient(
      "LOBBY"
    )

    ready = false
  }

  showCard("lobby")
  updateLobbyPlayers()
}

def returnToMenu(): Unit = {

  gameRunning = false

  sendClient(
    "LOBBY"
  )

  disconnectClient()
  stopServer()

  multiplayer = false
  soloOnline = false
  ready = false

  showCard("menu")
}

def exitApplication(): Unit = {

  try {
    disconnectClient()
  } catch {
    case _: Throwable =>
  }

  try {
    stopServer()
  } catch {
    case _: Throwable =>
  }

  try {
    frame.dispose()
  } catch {
    case _: Throwable =>
  }
}


/* =========================================================
   BUTTON
   ========================================================= */

def makeButton(
  text: String,
  width: Int,
  height: Int
): JButton = {

  val button =
    new JButton(text)

  button.setPreferredSize(
    new Dimension(
      width,
      height
    )
  )

  button.setFocusPainted(false)

  button.setFont(
    new Font(
      "Arial",
      Font.BOLD,
      15
    )
  )

  button
}


/* =========================================================
   MAIN MENU
   ========================================================= */

def makeMenu(): JPanel = {

  val panel =
    new JPanel()

  panel.setLayout(
    new BoxLayout(
      panel,
      BoxLayout.Y_AXIS
    )
  )

  panel.setBorder(
    BorderFactory
      .createEmptyBorder(
        35,
        80,
        35,
        80
      )
  )

  val title =
    new JLabel(
      "REAL DRIVE X"
    )

  title.setFont(
    new Font(
      "Arial",
      Font.BOLD,
      42
    )
  )

  title.setAlignmentX(
    0.5f
  )

  panel.add(title)

  val subtitle =
    new JLabel(
      "ONLINE ? MULTIPLAYER ? SOLO ? TRAFFIC"
    )

  subtitle.setAlignmentX(
    0.5f
  )

  panel.add(subtitle)

  panel.add(
    javax.swing.Box
      .createVerticalStrut(30)
  )

  val host =
    makeButton(
      "HOST ONLINE",
      330,
      55
    )

  host.setAlignmentX(
    0.5f
  )

  host.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        val enteredName =
          JOptionPane.showInputDialog(
            frame,
            "Driver name:",
            "Driver"
          )

        if (
          enteredName != null &&
          enteredName.trim.nonEmpty
        ) {
          playerName =
            cleanText(
              enteredName.trim
            )
        }

        val enteredPort =
          JOptionPane.showInputDialog(
            frame,
            "Server port:",
            "5555"
          )

        val port =
          Try(
            Option(
              enteredPort
            ).getOrElse(
              "5555"
            ).toInt
          ).getOrElse(
            5555
          )

        val started =
          startServer(port)

        if (started) {

          val connected =
            connectToServer(
              "127.0.0.1",
              actualServerPort
            )

          if (connected) {

            lobbyStatusLabel.setText(
              "HOST CONNECTED ? " +
              localIP() +
              ":" +
              actualServerPort
            )

            showCard("lobby")

          } else {

            lobbyStatusLabel.setText(
              "LOCAL HOST CONNECTION FAILED"
            )
          }

        } else {

          JOptionPane.showMessageDialog(
            frame,
            "Could not open server port."
          )
        }
      }
    }
  )

  panel.add(host)

  panel.add(
    javax.swing.Box
      .createVerticalStrut(12)
  )

  val join =
    makeButton(
      "JOIN ONLINE",
      330,
      55
    )

  join.setAlignmentX(
    0.5f
  )

  join.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        val enteredName =
          JOptionPane.showInputDialog(
            frame,
            "Driver name:",
            "Driver"
          )

        if (
          enteredName != null &&
          enteredName.trim.nonEmpty
        ) {
          playerName =
            cleanText(
              enteredName.trim
            )
        }

        val host =
          JOptionPane.showInputDialog(
            frame,
            "Host IP:",
            lastHost
          )

        if (
          host == null ||
          host.trim.isEmpty
        ) {
          return
        }

        val enteredPort =
          JOptionPane.showInputDialog(
            frame,
            "Port:",
            lastPort.toString
          )

        val port =
          Try(
            Option(
              enteredPort
            ).getOrElse(
              lastPort.toString
            ).toInt
          ).getOrElse(
            lastPort
          )

        lobbyStatusLabel.setText(
          "CONNECTING..."
        )

        val connected =
          connectToServer(
            host.trim,
            port
          )

        if (connected) {

          lobbyStatusLabel.setText(
            "CONNECTED TO " +
            host.trim +
            ":" +
            port
          )

          showCard("lobby")

        } else {

          lobbyStatusLabel.setText(
            "CONNECTION FAILED"
          )

          JOptionPane.showMessageDialog(
            frame,
            "Connection failed.\nCheck host IP, port and firewall."
          )
        }
      }
    }
  )

  panel.add(join)

  panel.add(
    javax.swing.Box
      .createVerticalStrut(12)
  )

  val offline =
    makeButton(
      "OFFLINE SOLO",
      330,
      55
    )

  offline.setAlignmentX(
    0.5f
  )

  offline.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        online = false
        multiplayer = false
        soloOnline = false

        startGame()
      }
    }
  )

  panel.add(offline)

  panel.add(
    javax.swing.Box
      .createVerticalStrut(12)
  )

  val exit =
    makeButton(
      "EXIT",
      330,
      50
    )

  exit.setAlignmentX(
    0.5f
  )

  exit.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        exitApplication()
      }
    }
  )

  panel.add(exit)

  panel
}


/* =========================================================
   LOBBY
   ========================================================= */

def makeLobby(): JPanel = {

  val panel =
    new JPanel()

  panel.setLayout(
    new BoxLayout(
      panel,
      BoxLayout.Y_AXIS
    )
  )

  panel.setBorder(
    BorderFactory
      .createEmptyBorder(
        20,
        30,
        20,
        30
      )
  )

  val title =
    new JLabel(
      "ONLINE LOBBY"
    )

  title.setFont(
    new Font(
      "Arial",
      Font.BOLD,
      34
    )
  )

  title.setAlignmentX(
    0.5f
  )

  panel.add(title)

  lobbyStatusLabel =
    new JLabel(
      "NOT CONNECTED"
    )

  lobbyStatusLabel.setAlignmentX(
    0.5f
  )

  panel.add(
    lobbyStatusLabel
  )

  lobbyPlayersLabel =
    new JLabel(
      "PLAYERS: 0"
    )

  lobbyPlayersLabel.setAlignmentX(
    0.5f
  )

  panel.add(
    lobbyPlayersLabel
  )

  lobbyModeLabel =
    new JLabel(
      "Choose an online mode."
    )

  lobbyModeLabel.setAlignmentX(
    0.5f
  )

  panel.add(
    lobbyModeLabel
  )

  panel.add(
    javax.swing.Box
      .createVerticalStrut(10)
  )

  val form =
    new JPanel()

  form.add(
    new JLabel("NAME")
  )

  lobbyNameField =
    new JTextField(
      "Driver",
      12
    )

  form.add(
    lobbyNameField
  )

  form.add(
    new JLabel("HOST")
  )

  lobbyHostField =
    new JTextField(
      "127.0.0.1",
      12
    )

  form.add(
    lobbyHostField
  )

  form.add(
    new JLabel("PORT")
  )

  lobbyPortField =
    new JTextField(
      "5555",
      7
    )

  form.add(
    lobbyPortField
  )

  panel.add(form)

  val modeRow =
    new JPanel()

  val solo =
    makeButton(
      "ONLINE SOLO",
      175,
      45
    )

  solo.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        playerName =
          cleanText(
            lobbyNameField.getText
          )

        if (
          playerName.isEmpty
        ) {
          playerName = "Driver"
        }

        startOnlineSolo()
      }
    }
  )

  modeRow.add(solo)

  val multi =
    makeButton(
      "MULTIPLAYER",
      175,
      45
    )

  multi.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        playerName =
          cleanText(
            lobbyNameField.getText
          )

        if (
          playerName.isEmpty
        ) {
          playerName = "Driver"
        }

        startMultiplayer()
      }
    }
  )

  modeRow.add(multi)

  val readyButton =
    makeButton(
      "READY",
      120,
      45
    )

  readyButton.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        ready = true

        sendClient(
          "READY"
        )

        addLobbyChat(
          "[LOCAL] READY"
        )
      }
    }
  )

  modeRow.add(
    readyButton
  )

  panel.add(modeRow)

  val chatLabel =
    new JLabel(
      "CHAT"
    )

  chatLabel.setAlignmentX(
    0.5f
  )

  panel.add(chatLabel)

  lobbyChatArea =
    new JTextArea(
      10,
      62
    )

  lobbyChatArea.setEditable(false)

  panel.add(
    new JScrollPane(
      lobbyChatArea
    )
  )

  val chatRow =
    new JPanel()

  lobbyChatField =
    new JTextField(
      38
    )

  val sendButton =
    makeButton(
      "SEND",
      100,
      40
    )

  def sendCurrentChat(): Unit = {

    val text =
      lobbyChatField
        .getText
        .trim

    if (
      text.nonEmpty
    ) {

      sendChat(text)

      lobbyChatField.setText("")
    }
  }

  sendButton.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        sendCurrentChat()
      }
    }
  )

  lobbyChatField.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        sendCurrentChat()
      }
    }
  )

  chatRow.add(
    lobbyChatField
  )

  chatRow.add(
    sendButton
  )

  panel.add(chatRow)

  val bottom =
    new JPanel()

  val retry =
    makeButton(
      "RETRY",
      120,
      42
    )

  retry.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        lobbyStatusLabel.setText(
          "RECONNECTING..."
        )

        val host =
          lobbyHostField
            .getText
            .trim

        val port =
          Try(
            lobbyPortField
              .getText
              .trim
              .toInt
          ).getOrElse(
            5555
          )

        val connected =
          connectToServer(
            host,
            port
          )

        if (connected) {

          lobbyStatusLabel.setText(
            "CONNECTED"
          )

        } else {

          lobbyStatusLabel.setText(
            "RECONNECT FAILED"
          )
        }
      }
    }
  )

  bottom.add(retry)

  val menu =
    makeButton(
      "BACK TO MENU",
      160,
      42
    )

  menu.addActionListener(
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        returnToMenu()
      }
    }
  )

  bottom.add(menu)

  panel.add(bottom)

  panel
}


/* =========================================================
   DRIVE PANEL
   ========================================================= */

class DrivePanel
  extends JPanel {

  setFocusable(true)

  addKeyListener(
    new KeyAdapter {

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

        val k =
          e.getKeyCode

        keys += k

        if (
          k == KeyEvent.VK_ESCAPE
        ) {

          leaveToLobby()
          return
        }

        if (
          k == KeyEvent.VK_SPACE
        ) {
          nitro = true
        }

        if (
          k == KeyEvent.VK_H
        ) {

          horn = true

          playTone(
            560,
            120
          )
        }

        if (
          k == KeyEvent.VK_L
        ) {
          toggleLights()
        }

        if (
          k == KeyEvent.VK_B
        ) {
          toggleHighBeam()
        }

        if (
          k == KeyEvent.VK_G
        ) {
          toggleAutoGear()
        }

        if (
          k == KeyEvent.VK_R
        ) {
          manualReverse()
        }

        if (
          k == KeyEvent.VK_N
        ) {

          if (!autoGear) {
            gear = "N"
          }
        }

        if (
          k == KeyEvent.VK_1
        ) {

          if (!autoGear) {
            gear = "1"
          }
        }

        if (
          k == KeyEvent.VK_2
        ) {

          if (!autoGear) {
            gear = "2"
          }
        }

        if (
          k == KeyEvent.VK_3
        ) {

          if (!autoGear) {
            gear = "3"
          }
        }

        if (
          k == KeyEvent.VK_4
        ) {

          if (!autoGear) {
            gear = "4"
          }
        }

        if (
          k == KeyEvent.VK_5
        ) {

          if (!autoGear) {
            gear = "5"
          }
        }

        if (
          k == KeyEvent.VK_P
        ) {
          paused = !paused
        }

        if (
          k == KeyEvent.VK_T
        ) {
          toggleRain()
        }
      }

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

        val k =
          e.getKeyCode

        keys -= k

        if (
          k == KeyEvent.VK_SPACE
        ) {
          nitro = false
        }

        if (
          k == KeyEvent.VK_H
        ) {
          horn = false
        }
      }
    }
  )


  /* =======================================================
     MOUSE / TOUCH
     ======================================================= */

  addMouseListener(
    new MouseAdapter {

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

        val x =
          e.getX

        val y =
          e.getY

        val h =
          getHeight

        val w =
          getWidth

        if (
          y > h - 95
        ) {

          if (
            x < 120
          ) {

            steering = -1.0
            return

          } else if (
            x < 240
          ) {

            braking = true
            return

          } else if (
            x > w - 240 &&
            x < w - 120
          ) {

            throttle = true
            return

          } else if (
            x > w - 120
          ) {

            steering = 1.0
            return
          }
        }

        if (
          y < 80
        ) {

          if (
            x < 100
          ) {

            toggleLights()
            return

          } else if (
            x < 205
          ) {

            toggleHighBeam()
            return

          } else if (
            x < 315
          ) {

            nitro = true
            return

          } else if (
            x < 425
          ) {

            horn = true

            playTone(
              560,
              120
            )

            return
          }
        }
      }

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

        throttle = false
        braking = false
        nitro = false
        horn = false
        steering = 0.0
      }
    }
  )


  /* =======================================================
     DRAW
     ======================================================= */

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

    super.paintComponent(
      graphics
    )

    val g =
      graphics.asInstanceOf[
        Graphics2D
      ]

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

    drawSky(g)
    drawRoad(g)
    drawTraffic(g)
    drawRemoteCars(g)
    drawRain(g)
    drawCockpit(g)
    drawHud(g)
    drawControls(g)

    if (paused) {
      drawPause(g)
    }

    if (
      flashTime >
      System.currentTimeMillis()
    ) {
      drawFlash(g)
    }
  }


  /* =======================================================
     SKY
     ======================================================= */

  def drawSky(
    g: Graphics2D
  ): Unit = {

    val w =
      getWidth

    val h =
      getHeight

    var top =
      new Color(
        70,
        155,
        230
      )

    var bottom =
      new Color(
        200,
        230,
        250
      )

    if (timePhase == 1) {

      top =
        new Color(
          235,
          125,
          75
        )

      bottom =
        new Color(
          255,
          215,
          145
        )
    }

    if (timePhase == 2) {

      top =
        new Color(
          10,
          18,
          45
        )

      bottom =
        new Color(
          50,
          60,
          100
        )
    }

    if (timePhase == 3) {

      top =
        new Color(
          110,
          65,
          125
        )

      bottom =
        new Color(
          210,
          120,
          130
        )
    }

    g.setPaint(
      new GradientPaint(
        0,
        0,
        top,
        0,
        h,
        bottom
      )
    )

    g.fillRect(
      0,
      0,
      w,
      h
    )

    if (
      timePhase == 2
    ) {
      drawStars(g)
    }

    drawSunMoon(g)
  }

  def drawStars(
    g: Graphics2D
  ): Unit = {

    var i = 0

    while (i < 70) {

      val x =
        (
          i * 89
        ) %
        Math.max(
          1,
          getWidth
        )

      val y =
        15 +
        (
          i * 43
        ) %
        Math.max(
          1,
          getHeight / 2
        )

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

      g.fillOval(
        x,
        y,
        2 + i % 3,
        2 + i % 3
      )

      i += 1
    }
  }

  def drawSunMoon(
    g: Graphics2D
  ): Unit = {

    if (
      timePhase == 2
    ) {

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

      g.fillOval(
        getWidth - 145,
        55,
        72,
        72
      )

    } else {

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

      g.fillOval(
        getWidth - 155,
        50,
        88,
        88
      )
    }
  }


  /* =======================================================
     ROAD
     ======================================================= */

  def drawRoad(
    g: Graphics2D
  ): Unit = {

    val w =
      getWidth

    val h =
      getHeight

    val horizon =
      h / 3

    g.setColor(
      new Color(
        55,
        105,
        55
      )
    )

    g.fillRect(
      0,
      horizon,
      w,
      h - horizon
    )

    val topY =
      horizon - 20

    val bottomY =
      h

    val topLeft =
      w / 2 - 85

    val topRight =
      w / 2 + 85

    val bottomLeft =
      w / 2 - 270

    val bottomRight =
      w / 2 + 270

    val polygon =
      new java.awt.Polygon()

    polygon.addPoint(
      topLeft,
      topY
    )

    polygon.addPoint(
      topRight,
      topY
    )

    polygon.addPoint(
      bottomRight,
      bottomY
    )

    polygon.addPoint(
      bottomLeft,
      bottomY
    )

    g.setColor(
      new Color(
        45,
        46,
        50
      )
    )

    g.fillPolygon(
      polygon
    )

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

    g.setStroke(
      new BasicStroke(
        7.0f
      )
    )

    g.drawLine(
      topLeft,
      topY,
      bottomLeft,
      bottomY
    )

    g.drawLine(
      topRight,
      topY,
      bottomRight,
      bottomY
    )

    drawLane(
      g,
      topLeft,
      topRight,
      bottomLeft,
      bottomRight,
      topY,
      bottomY,
      1.0 / 3.0
    )

    drawLane(
      g,
      topLeft,
      topRight,
      bottomLeft,
      bottomRight,
      topY,
      bottomY,
      2.0 / 3.0
    )

    drawRoadside(
      g,
      horizon
    )
  }

  def drawLane(
    g: Graphics2D,
    tl: Int,
    tr: Int,
    bl: Int,
    br: Int,
    topY: Int,
    bottomY: Int,
    fraction: Double
  ): Unit = {

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

    var i = 0

    while (i < 10) {

      if (
        i % 2 == 0
      ) {

        val y1 =
          topY +
          (
            (
              bottomY -
              topY
            ) *
            i.toDouble /
            10.0
          ).toInt

        val y2 =
          topY +
          (
            (
              bottomY -
              topY
            ) *
            (
              i + 0.5
            ).toDouble /
            10.0
          ).toInt

        val x1 =
          interpolateX(
            tl,
            tr,
            bl,
            br,
            topY,
            bottomY,
            y1,
            fraction
          )

        val x2 =
          interpolateX(
            tl,
            tr,
            bl,
            br,
            topY,
            bottomY,
            y2,
            fraction
          )

        g.setStroke(
          new BasicStroke(
            (
              2.0 +
              i * 0.7
            ).toFloat
          )
        )

        g.drawLine(
          x1,
          y1,
          x2,
          y2
        )
      }

      i += 1
    }
  }

  def interpolateX(
    tl: Int,
    tr: Int,
    bl: Int,
    br: Int,
    topY: Int,
    bottomY: Int,
    y: Int,
    fraction: Double
  ): Int = {

    val t =
      (
        y - topY
      ).toDouble /
      Math.max(
        1,
        bottomY - topY
      ).toDouble

    val left =
      tl +
      (
        (
          bl - tl
        ) *
        t
      ).toInt

    val right =
      tr +
      (
        (
          br - tr
        ) *
        t
      ).toInt

    left +
    (
      (
        right - left
      ) *
      fraction
    ).toInt
  }

  def drawRoadside(
    g: Graphics2D,
    horizon: Int
  ): Unit = {

    var i = 0

    while (i < 9) {

      val y =
        horizon +
        25 +
        i * 58

      val left =
        60 +
        i * 14

      val right =
        getWidth -
        75 -
        i * 14

      g.setColor(
        new Color(
          35,
          90,
          40
        )
      )

      g.fillRect(
        left,
        y,
        8,
        45
      )

      g.fillOval(
        left - 18,
        y - 25,
        45,
        38
      )

      g.fillRect(
        right,
        y,
        8,
        45
      )

      g.fillOval(
        right - 18,
        y - 25,
        45,
        38
      )

      i += 1
    }
  }


  /* =======================================================
     TRAFFIC DRAW
     ======================================================= */

  def drawTraffic(
    g: Graphics2D
  ): Unit = {

    var i = 0

    while (
      i < traffic.length
    ) {

      drawTrafficCar(
        g,
        traffic(i)
      )

      i += 1
    }
  }

  def drawTrafficCar(
    g: Graphics2D,
    car: TrafficCar
  ): Unit = {

    val w =
      getWidth

    val h =
      getHeight

    val horizon =
      h / 3

    val z =
      Math.max(
        0.0,
        Math.min(
          1.0,
          car.z
        )
      )

    val y =
      horizon +
      (
        h - horizon
      ) *
      z

    val roadHalf =
      90.0 +
      170.0 *
      z

    val cx =
      w / 2.0 +
      laneToX(
        car.lane
      ) *
      roadHalf

    val cw =
      20.0 +
      48.0 *
      z

    val ch =
      16.0 +
      66.0 *
      z

    val x =
      cx -
      cw / 2.0

    val top =
      y -
      ch / 2.0

    var body =
      new Color(
        220,
        35,
        45
      )

    if (
      car.kind == "TRUCK"
    ) {

      body =
        new Color(
          190,
          130,
          55
        )
    }

    if (
      car.kind == "BUS"
    ) {

      body =
        new Color(
          55,
          110,
          210
        )
    }

    if (
      car.police
    ) {

      body =
        new Color(
          235,
          235,
          240
        )
    }

    g.setColor(body)

    g.fillRoundRect(
      x.toInt,
      top.toInt,
      cw.toInt,
      ch.toInt,
      10,
      10
    )

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

    g.fillRect(
      (
        x +
        cw * 0.18
      ).toInt,
      (
        top +
        ch * 0.25
      ).toInt,
      (
        cw * 0.64
      ).toInt,
      (
        ch * 0.27
      ).toInt
    )

    if (
      car.police
    ) {

      g.setColor(
        Color.RED
      )

      g.fillRect(
        (
          x +
          cw * 0.18
        ).toInt,
        (
          top +
          ch * 0.12
        ).toInt,
        (
          cw * 0.27
        ).toInt,
        (
          ch * 0.12
        ).toInt
      )

      g.setColor(
        Color.BLUE
      )

      g.fillRect(
        (
          x +
          cw * 0.55
        ).toInt,
        (
          top +
          ch * 0.12
        ).toInt,
        (
          cw * 0.27
        ).toInt,
        (
          ch * 0.12
        ).toInt
      )
    }
  }


  /* =======================================================
     REMOTE PLAYERS
     ======================================================= */

  def drawRemoteCars(
    g: Graphics2D
  ): Unit = {

    if (!multiplayer) {
      return
    }

    remotePlayers.synchronized {

      var i = 0

      while (
        i < remotePlayers.length
      ) {

        val p =
          remotePlayers(i)

        val relative =
          p.distance -
          gameDistance

        if (
          Math.abs(relative) < 700.0
        ) {

          val z =
            Math.max(
              0.20,
              Math.min(
                1.0,
                0.62 +
                relative /
                1400.0
              )
            )

          val roadHalf =
            90.0 +
            170.0 *
            z

          val cx =
            getWidth / 2.0 +
            p.x *
            roadHalf

          val y =
            getHeight / 3 +
            (
              getHeight -
              getHeight / 3
            ) *
            z

          val cw =
            25.0 +
            35.0 *
            z

          val ch =
            34.0 +
            45.0 *
            z

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

          g.fillRoundRect(
            (
              cx -
              cw / 2.0
            ).toInt,
            (
              y -
              ch / 2.0
            ).toInt,
            cw.toInt,
            ch.toInt,
            12,
            12
          )

          g.setColor(
            Color.WHITE
          )

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

          g.drawString(
            p.name,
            (
              cx -
              25
            ).toInt,
            (
              y -
              ch / 2.0 -
              6
            ).toInt
          )
        }

        i += 1
      }
    }
  }


  /* =======================================================
     RAIN
     ======================================================= */

  def drawRain(
    g: Graphics2D
  ): Unit = {

    if (!rain) {
      return
    }

    g.setColor(
      new Color(
        180,
        210,
        255,
        125
      )
    )

    var i = 0

    while (i < 130) {

      val x =
        (
          i * 97 +
          System.currentTimeMillis() /
          7L
        ).toInt %
        Math.max(
          1,
          getWidth
        )

      val y =
        (
          i * 43 +
          System.currentTimeMillis() /
          5L
        ).toInt %
        Math.max(
          1,
          getHeight
        )

      g.drawLine(
        x,
        y,
        x - 5,
        y + 17
      )

      i += 1
    }
  }


  /* =======================================================
     COCKPIT
     ======================================================= */

  def drawCockpit(
    g: Graphics2D
  ): Unit = {

    val w =
      getWidth

    val h =
      getHeight

    g.setColor(
      new Color(
        15,
        18,
        23,
        220
      )
    )

    g.fillRoundRect(
      w / 2 - 245,
      h - 165,
      490,
      150,
      28,
      28
    )

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

    g.fillOval(
      w / 2 - 125,
      h - 130,
      105,
      105
    )

    g.fillOval(
      w / 2 + 25,
      h - 130,
      105,
      105
    )

    g.setColor(
      Color.WHITE
    )

    g.setStroke(
      new BasicStroke(
        7.0f
      )
    )

    g.drawOval(
      w / 2 - 125,
      h - 130,
      105,
      105
    )

    g.drawOval(
      w / 2 + 25,
      h - 130,
      105,
      105
    )

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

    g.drawString(
      "KM/H",
      w / 2 - 106,
      h - 40
    )

    g.drawString(
      "GEAR",
      w / 2 + 50,
      h - 105
    )

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

    g.drawString(
      Math.abs(
        gameSpeed
      ).toInt.toString,
      w / 2 - 104,
      h - 70
    )

    g.drawString(
      gear,
      w / 2 + 67,
      h - 65
    )
  }


  /* =======================================================
     HUD
     ======================================================= */

  def drawHud(
    g: Graphics2D
  ): Unit = {

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

    g.fillRoundRect(
      15,
      95,
      285,
      205,
      18,
      18
    )

    g.setColor(
      Color.WHITE
    )

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

    g.drawString(
      "REAL DRIVE X",
      30,
      120
    )

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

    g.drawString(
      "MODE: " +
      modeText(),
      30,
      145
    )

    g.drawString(
      "SPEED: " +
      Math.abs(
        gameSpeed
      ).toInt +
      " km/h",
      30,
      167
    )

    g.drawString(
      "DISTANCE: " +
      gameDistance.toInt +
      " m",
      30,
      189
    )

    g.drawString(
      "SCORE: " +
      score,
      30,
      211
    )

    g.drawString(
      "FUEL: " +
      fuel.toInt +
      "%",
      30,
      233
    )

    g.drawString(
      "DAMAGE: " +
      damage.toInt +
      "%",
      30,
      255
    )

    g.drawString(
      "MISSION " +
      missionNumber +
      "  " +
      missionProgress +
      "/" +
      missionTarget.toInt,
      30,
      277
    )

    drawGPS(g)
  }


  /* =======================================================
     GPS
     ======================================================= */

  def drawGPS(
    g: Graphics2D
  ): Unit = {

    val x =
      getWidth - 185

    val y =
      95

    g.setColor(
      new Color(
        8,
        18,
        22,
        235
      )
    )

    g.fillRoundRect(
      x,
      y,
      165,
      185,
      18,
      18
    )

    g.setColor(
      new Color(
        60,
        155,
        80
      )
    )

    g.fillRoundRect(
      x + 18,
      y + 18,
      129,
      118,
      18,
      18
    )

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

    g.fillRect(
      x + 76,
      y + 18,
      25,
      116
    )

    g.setColor(
      Color.WHITE
    )

    g.fillOval(
      x + 85,
      y + 88,
      9,
      9
    )

    g.setColor(
      Color.RED
    )

    g.fillOval(
      x + 100,
      y + 48,
      11,
      11
    )

    g.setColor(
      Color.WHITE
    )

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

    g.drawString(
      "GPS / MAP",
      x + 42,
      y + 157
    )

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

    g.drawString(
      "DESTINATION",
      x + 39,
      y + 176
    )
  }


  /* =======================================================
     TOUCH BUTTONS
     ======================================================= */

  def drawControls(
    g: Graphics2D
  ): Unit = {

    val w =
      getWidth

    val h =
      getHeight

    drawControlButton(
      g,
      18,
      h - 88,
      92,
      58,
      "LEFT"
    )

    drawControlButton(
      g,
      122,
      h - 88,
      92,
      58,
      "BRAKE"
    )

    drawControlButton(
      g,
      w - 214,
      h - 88,
      92,
      58,
      "GAS"
    )

    drawControlButton(
      g,
      w - 110,
      h - 88,
      92,
      58,
      "RIGHT"
    )

    drawControlButton(
      g,
      18,
      18,
      88,
      48,
      "LIGHT"
    )

    drawControlButton(
      g,
      116,
      18,
      88,
      48,
      "BEAM"
    )

    drawControlButton(
      g,
      214,
      18,
      88,
      48,
      "NITRO"
    )

    drawControlButton(
      g,
      312,
      18,
      88,
      48,
      "HORN"
    )
  }

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

    g.setColor(
      new Color(
        15,
        20,
        30,
        200
      )
    )

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

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

    g.setStroke(
      new BasicStroke(
        2.0f
      )
    )

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

    g.setColor(
      Color.WHITE
    )

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

    val textWidth =
      g.getFontMetrics
        .stringWidth(text)

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


  /* =======================================================
     PAUSE
     ======================================================= */

  def drawPause(
    g: Graphics2D
  ): Unit = {

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

    g.fillRect(
      0,
      0,
      getWidth,
      getHeight
    )

    g.setColor(
      Color.WHITE
    )

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

    g.drawString(
      "PAUSED",
      getWidth / 2 - 105,
      getHeight / 2
    )
  }


  /* =======================================================
     FLASH
     ======================================================= */

  def drawFlash(
    g: Graphics2D
  ): Unit = {

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

    g.fillRoundRect(
      getWidth / 2 - 170,
      72,
      340,
      55,
      17,
      17
    )

    g.setColor(
      Color.WHITE
    )

    g.setFont(
      new Font(
        "Arial",
        Font.BOLD,
        20
      )
    )

    val tw =
      g.getFontMetrics
        .stringWidth(
          flashMessage
        )

    g.drawString(
      flashMessage,
      getWidth / 2 -
      tw / 2,
      107
    )
  }
}


/* =========================================================
   CREATE UI
   ========================================================= */

frame =
  new JFrame(
    "REAL DRIVE X"
  )

frame.setDefaultCloseOperation(
  WindowConstants.EXIT_ON_CLOSE
)

frame.setSize(
  1280,
  820
)

frame.setLocationRelativeTo(
  null
)

cards =
  new java.awt.CardLayout()

root =
  new JPanel(
    cards
  )

menuPanel =
  makeMenu()

lobbyPanel =
  makeLobby()

gamePanel =
  new DrivePanel()

root.add(
  menuPanel,
  "menu"
)

root.add(
  lobbyPanel,
  "lobby"
)

root.add(
  gamePanel,
  "game"
)

frame.setContentPane(
  root
)

frame.setVisible(
  true
)

showCard(
  "menu"
)


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

val gameTimer =
  new javax.swing.Timer(
    33,
    new ActionListener {

      def actionPerformed(
        e: java.awt.event.ActionEvent
      ): Unit = {

        updateGame()

        if (
          gamePanel != null
        ) {

          gamePanel.repaint()
        }

        if (
          flashTime > 0L &&
          flashTime <
          System.currentTimeMillis()
        ) {

          flashTime = 0L
        }
      }
    }
  )

gameTimer.start()


/* =========================================================
   WINDOW FOCUS
   ========================================================= */

frame.addWindowFocusListener(
  new java.awt.event.WindowAdapter {

    override def windowGainedFocus(
      e: java.awt.event.WindowEvent
    ): Unit = {

      if (
        gamePanel != null
      ) {

        gamePanel.requestFocusInWindow()
      }
    }

    override def windowClosing(
      e: java.awt.event.WindowEvent
    ): Unit = {

      exitApplication()
    }
  }
)