Code Sketch


yoiii
By: Mhalsakant School
Category: Programming
import java.awt._
import java.awt.event._
import java.awt.geom._
import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Paths}
import java.security.MessageDigest
import java.util.Base64
import javax.swing._
import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.Set
import scala.util.Random


// ============================================================
// DESI TRUCK BATTLE ROYALE - ORIGINAL SCALA/SWING GAME
// Flow: LOGIN -> GARAGE -> CITY -> AIRPORT -> AIRPLANE
//       -> LAND -> BATTLE ARENA -> RESULT -> PROFILE
// Local 2-player mode included. Progress is saved locally.
// ============================================================

val WIDTH = 1280
val HEIGHT = 760
val WORLD_W = 1800.0
val WORLD_H = 1200.0

val BG = new Color(16, 20, 26)
val DARK = new Color(24, 29, 36)
val PANEL = new Color(33, 39, 48)
val PANEL2 = new Color(45, 52, 63)
val TEXT = new Color(238, 242, 246)
val MUTED = new Color(164, 174, 184)
val GOLD = new Color(242, 194, 65)
val ROAD = new Color(65, 67, 71)
val ROAD_MARK = new Color(220, 198, 105)
val GRASS = new Color(112, 155, 88)
val GRASS2 = new Color(98, 143, 80)
val WATER = new Color(65, 125, 165)
val HOUSE = new Color(179, 151, 115)
val ROOF = new Color(111, 72, 58)
val TRUCK_RED = new Color(211, 58, 45)
val TRUCK_DARK = new Color(120, 34, 28)
val ENEMY = new Color(201, 65, 78)
val FRIEND = new Color(55, 179, 101)
val PLAYER2 = new Color(72, 153, 222)
val SKY = new Color(137, 201, 235)
val CLOUD = new Color(247, 250, 252, 190)
val SAFE = new Color(67, 190, 125)
val BLUE = new Color(91, 162, 226)

// ------------------------------------------------------------
// ACCOUNT / PROFILE STATE
// ------------------------------------------------------------
case class Account(
  username: String,
  playerId: String,
  passwordHash: String,
  points: Int,
  coins: Int,
  matches: Int,
  bestKills: Int
)

val saveFile = Paths.get(
  System.getProperty("user.home"),
  ".desi_truck_battle_accounts.txt"
)

var accounts = ArrayBuffer[Account]()
var currentUsername = ""
var currentPlayerId = ""
var currentPoints = 0
var currentCoins = 500
var totalMatches = 0
var currentKills = 0
var bestKills = 0

def hashPassword(text: String): String = {
  val md = MessageDigest.getInstance("SHA-256")
  md.digest(text.getBytes(StandardCharsets.UTF_8)).map("%02x".format(_)).mkString
}

def b64(text: String): String =
  Base64.getEncoder.encodeToString(text.getBytes(StandardCharsets.UTF_8))

def unb64(text: String): String =
  new String(Base64.getDecoder.decode(text), StandardCharsets.UTF_8)

def ensureSaveFile(): Unit = {
  try {
    if (!Files.exists(saveFile)) Files.createFile(saveFile)
  } catch {
    case _: Throwable => ()
  }
}

def loadAccounts(): Unit = {
  ensureSaveFile()
  try {
    val lines = Files.readAllLines(saveFile, StandardCharsets.UTF_8)
    val it = lines.iterator()
    while (it.hasNext) {
      val p = it.next().split("\\|", -1)
      if (p.length >= 7) {
        try {
          accounts += Account(
            unb64(p(0)),
            unb64(p(1)),
            unb64(p(2)),
            p(3).toInt,
            p(4).toInt,
            p(5).toInt,
            p(6).toInt
          )
        } catch {
          case _: Throwable => ()
        }
      }
    }
  } catch {
    case _: Throwable => ()
  }
}

def saveAccounts(): Unit = {
  try {
    val text = accounts.map { a =>
      Seq(
        b64(a.username),
        b64(a.playerId),
        b64(a.passwordHash),
        a.points.toString,
        a.coins.toString,
        a.matches.toString,
        a.bestKills.toString
      ).mkString("|")
    }.mkString("\n")
    Files.write(saveFile, text.getBytes(StandardCharsets.UTF_8))
  } catch {
    case _: Throwable => ()
  }
}

def uniquePlayerId(): String = {
  var id = ""
  var used = true
  while (used) {
    id = "IND-" + (100000 + rng.nextInt(900000)).toString
    used = accounts.exists(_.playerId == id)
  }
  id
}

def saveCurrent(): Unit = {
  val idx = accounts.indexWhere(_.username.equalsIgnoreCase(currentUsername))
  if (idx >= 0) {
    val old = accounts(idx)
    accounts.update(
      idx,
      old.copy(
        points = currentPoints,
        coins = currentCoins,
        matches = totalMatches,
        bestKills = Math.max(bestKills, old.bestKills)
      )
    )
    saveAccounts()
  }
}

// ------------------------------------------------------------
// WORLD DATA
// ------------------------------------------------------------
case class Building(x: Double, y: Double, w: Double, h: Double, name: String, floors: Int)
case class Traffic(x: Double, y: Double, dx: Double, dy: Double, kind: String)
case class Enemy(var x: Double, var y: Double, var hp: Int, var alive: Boolean, var cool: Int, name: String)
case class Loot(x: Double, y: Double, kind: String, amount: Int, var taken: Boolean)
case class Bullet(var x: Double, var y: Double, vx: Double, vy: Double, owner: Int, var alive: Boolean)

val rng = new Random()
val buildings = ArrayBuffer[Building]()
val traffic = ArrayBuffer[Traffic]()
val enemies = ArrayBuffer[Enemy]()
val loot = ArrayBuffer[Loot]()
val bullets = ArrayBuffer[Bullet]()
val held = Set[String]()

val gunNames = Array("Pistol", "SMG", "Rifle", "Shotgun")
val gunDamage = Array(18, 12, 25, 36)
val gunAmmo = Array(60, 120, 90, 42)
val gunCost = Array(0, 300, 650, 900)
val fireGap = Array(13, 7, 10, 18)

// ------------------------------------------------------------
// GAME FLOW STATE
// ------------------------------------------------------------
var screen = "LOGIN"
var message = "Welcome to DESI TRUCK BATTLE ROYALE"
var messageLife = 240

var truckX = 190.0
var truckY = 1060.0
var truckAngle = -Math.PI / 2.0
var truckSpeed = 0.0
var truckFuel = 100.0

var planeProgress = 0.0
var planeAltitude = 1000.0
var landReady = false
var flightBoost = 0

// Animation / transition counters
// These must exist at top level because the city crash and airplane landing
// states are updated from the shared game loop.
var crashTicks = 0
var landingTicks = 0

var playerX = 900.0
var playerY = 620.0
var playerAngle = 0.0
var hp = 100
var armor = 20
var medkits = 2
var ammo = 60
var gun = 0
var lootCount = 0
var shots = 0
var hits = 0
var battleTicks = 0
var battleActive = false
var twoPlayer = false
var p2X = 770.0
var p2Y = 620.0
var p2HP = 100
var gameWon = false

var tickNo = 0L
var lastTime = System.currentTimeMillis()

// ------------------------------------------------------------
// UI / WINDOW
// ------------------------------------------------------------
val frame = new JFrame("DESI TRUCK BATTLE ROYALE")
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE)
frame.setSize(WIDTH, HEIGHT)
frame.setResizable(false)
frame.setLocationRelativeTo(null)

var panel: GamePanel = null
var loginUser = new JTextField(18)
var loginPass = new JPasswordField(18)

def showMessage(text: String): Unit = {
  message = text
  messageLife = 220
}

def makeButton(label: String)(action: => Unit): JButton = {
  val b = new JButton(label)
  b.setFont(new Font("SansSerif", Font.BOLD, 15))
  b.setForeground(TEXT)
  b.setBackground(PANEL2)
  b.setFocusPainted(false)
  b.addActionListener(new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = action
  })
  b
}

// ------------------------------------------------------------
// LOGIN SCREEN
// ------------------------------------------------------------
def buildLogin(): Unit = {
  val root = new JPanel(new BorderLayout(22, 22))
  root.setBorder(BorderFactory.createEmptyBorder(25, 34, 25, 34))
  root.setBackground(BG)

  val title = new JLabel("DESI TRUCK BATTLE ROYALE", SwingConstants.CENTER)
  title.setForeground(GOLD)
  title.setFont(new Font("SansSerif", Font.BOLD, 36))
  root.add(title, BorderLayout.NORTH)

  val info = new JTextArea()
  info.setEditable(false)
  info.setLineWrap(true)
  info.setWrapStyleWord(true)
  info.setBackground(PANEL)
  info.setForeground(TEXT)
  info.setFont(new Font("SansSerif", Font.PLAIN, 16))
  info.setBorder(BorderFactory.createEmptyBorder(22, 22, 22, 22))
  info.setText(
    "DESI ROAD TO BATTLE\n\n" +
    "Create your own player account. Your Player ID, points, coins and match history are saved locally.\n\n" +
    "MISSION FLOW\n" +
    "1. Take your Indian-style cargo truck.\n" +
    "2. Drive through a large city with buildings, roads and traffic.\n" +
    "3. Reach the airport.\n" +
    "4. Enter the aircraft.\n" +
    "5. Fly until the LAND button becomes available.\n" +
    "6. Land at the original battle arena.\n" +
    "7. Explore houses and collect useful loot.\n" +
    "8. Fight enemy bots, survive the safe zone and earn points.\n" +
    "9. Results are saved to the same account.\n\n" +
    "LOCAL 2-PLAYER\n" +
    "P1 = WASD + SPACE\n" +
    "P2 = IJKL + U\n\n" +
    "This project uses original Java2D visuals and does not copy copyrighted game assets or maps."
  )

  val form = new JPanel()
  form.setBackground(PANEL)
  form.setLayout(new BoxLayout(form, BoxLayout.Y_AXIS))
  form.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24))

  val heading = new JLabel("PLAYER ACCOUNT")
  heading.setForeground(TEXT)
  heading.setFont(new Font("SansSerif", Font.BOLD, 21))
  form.add(heading)
  form.add(Box.createVerticalStrut(14))

  val ul = new JLabel("Username")
  ul.setForeground(MUTED)
  form.add(ul)
  loginUser = new JTextField(18)
  loginUser.setFont(new Font("SansSerif", Font.PLAIN, 16))
  form.add(loginUser)
  form.add(Box.createVerticalStrut(12))

  val pl = new JLabel("Password")
  pl.setForeground(MUTED)
  form.add(pl)
  loginPass = new JPasswordField(18)
  loginPass.setFont(new Font("SansSerif", Font.PLAIN, 16))
  form.add(loginPass)
  form.add(Box.createVerticalStrut(18))

  val sign = makeButton("SIGN IN") {
    val u = loginUser.getText.trim
    val p = loginPass.getPassword.mkString
    accounts.find(_.username.equalsIgnoreCase(u)) match {
      case Some(a) if a.passwordHash == hashPassword(p) =>
        currentUsername = a.username
        currentPlayerId = a.playerId
        currentPoints = a.points
        currentCoins = a.coins
        totalMatches = a.matches
        bestKills = a.bestKills
        currentKills = 0
        buildCity()
        screen = "GARAGE"
        showGame()
        showMessage("Welcome back, " + currentUsername + "!")
      case _ =>
        JOptionPane.showMessageDialog(frame, "Wrong username or password.", "SIGN IN", JOptionPane.WARNING_MESSAGE)
    }
  }

  val create = makeButton("NEW ACCOUNT") {
    val u = loginUser.getText.trim
    val p = loginPass.getPassword.mkString
    if (u.length < 3 || p.length < 4) {
      JOptionPane.showMessageDialog(frame, "Use at least 3 characters for username and 4 for password.")
    } else if (accounts.exists(_.username.equalsIgnoreCase(u))) {
      JOptionPane.showMessageDialog(frame, "That username already exists. Use SIGN IN.")
    } else {
      currentUsername = u
      currentPlayerId = uniquePlayerId()
      currentPoints = 0
      currentCoins = 500
      totalMatches = 0
      bestKills = 0
      currentKills = 0
      accounts += Account(currentUsername, currentPlayerId, hashPassword(p), 0, 500, 0, 0)
      saveAccounts()
      JOptionPane.showMessageDialog(
        frame,
        "Account created!\n\nUsername: " + currentUsername + "\nPlayer ID: " + currentPlayerId + "\nCoins: 500",
        "NEW PLAYER",
        JOptionPane.INFORMATION_MESSAGE
      )
      buildCity()
      screen = "GARAGE"
      showGame()
    }
  }

  val guest = makeButton("DEMO GUEST") {
    currentUsername = "Guest"
    currentPlayerId = "DEMO-000001"
    currentPoints = 0
    currentCoins = 500
    totalMatches = 0
    currentKills = 0
    bestKills = 0
    buildCity()
    screen = "GARAGE"
    showGame()
    showMessage("Demo guest started")
  }

  form.add(sign)
  form.add(Box.createVerticalStrut(10))
  form.add(create)
  form.add(Box.createVerticalStrut(10))
  form.add(guest)

  val split = new JPanel(new GridLayout(1, 2, 24, 0))
  split.setOpaque(false)
  split.add(info)
  split.add(form)
  root.add(split, BorderLayout.CENTER)

  val footer = new JLabel("Original game concept ? Offline/local save ? No copyrighted game files", SwingConstants.CENTER)
  footer.setForeground(MUTED)
  footer.setFont(new Font("SansSerif", Font.PLAIN, 12))
  root.add(footer, BorderLayout.SOUTH)

  frame.setContentPane(root)
  frame.revalidate()
  frame.repaint()
}

// ------------------------------------------------------------
// WORLD GENERATION
// ------------------------------------------------------------
def buildCity(): Unit = {
  crashTicks = 0
  landingTicks = 0
  landReady = false
  planeProgress = 0.0
  buildings.clear()
  traffic.clear()

  val names = Array(
    "Ganesh Chowk", "Market", "Workshop", "School", "Clinic",
    "Warehouse", "Truck Stand", "Small Hotel", "Factory Gate", "Temple Lane",
    "Bus Depot", "Old Market", "Farm House", "Service Center", "Airport Road"
  )

  val xs = Array(80, 365, 700, 1040, 1370, 80, 365, 700, 1040, 1370, 80, 365, 700, 1040, 1370)
  val ys = Array(80, 80, 80, 80, 80, 430, 430, 430, 430, 430, 780, 780, 780, 780, 780)

  var i = 0
  while (i < names.length) {
    buildings += Building(
      xs(i).toDouble,
      ys(i).toDouble,
      185 + (i % 3) * 36,
      155 + (i % 2) * 55,
      names(i),
      1 + i % 3
    )
    i += 1
  }

  // Big multi-storey landmarks
  buildings += Building(520, 115, 220, 300, "CITY MALL TOWER", 8)
  buildings += Building(1240, 120, 280, 320, "MAHANAGAR OFFICE", 10)
  buildings += Building(520, 670, 250, 250, "GRAND HOSPITAL", 7)
  buildings += Building(1220, 680, 300, 260, "MEGA WAREHOUSE", 6)

  var j = 0
  while (j < 30) {
    traffic += Traffic(
      (30 + ((j * 211) % 1740)).toDouble,
      (290 + ((j * 173) % 760)).toDouble,
      if (j % 2 == 0) 1.0 else -1.0,
      0.0,
      if (j % 5 == 0) "BUS" else if (j % 3 == 0) "AUTO" else "CAR"
    )
    j += 1
  }

  truckX = 190
  truckY = 1060
  truckAngle = -Math.PI / 2.0
  truckSpeed = 0
  truckFuel = 100
}

def buildBattle(): Unit = {
  landingTicks = 0
  enemies.clear()
  loot.clear()
  bullets.clear()

  val spawns = Array(
    (210, 190), (460, 190), (760, 230), (1080, 180), (1430, 230),
    (260, 510), (620, 500), (1010, 470), (1390, 520),
    (380, 850), (780, 880), (1160, 850), (1510, 880)
  )

  var i = 0
  while (i < spawns.length) {
    enemies += Enemy(
      spawns(i)._1.toDouble,
      spawns(i)._2.toDouble,
      75 + rng.nextInt(45),
      true,
      rng.nextInt(25),
      "ENEMY-" + (i + 1)
    )
    i += 1
  }

  val kinds = Array("AMMO", "MEDKIT", "ARMOR", "SMG", "RIFLE", "COINS")
  var j = 0
  while (j < 55) {
    loot += Loot(
      (90 + rng.nextInt(1620)).toDouble,
      (100 + rng.nextInt(980)).toDouble,
      kinds(rng.nextInt(kinds.length)),
      10 + rng.nextInt(45),
      false
    )
    j += 1
  }

  playerX = 900
  playerY = 1020
  playerAngle = -Math.PI / 2
  hp = 100
  armor = 20
  medkits = 2
  gun = 0
  ammo = gunAmmo(gun)
  lootCount = 0
  shots = 0
  hits = 0
  battleTicks = 0
  currentKills = 0
  gameWon = false
  battleActive = true
  twoPlayer = false
  p2X = 780
  p2Y = 1030
  p2HP = 100
}

// ------------------------------------------------------------
// GAME PANEL
// ------------------------------------------------------------
class GamePanel extends JPanel with KeyListener {
  setFocusable(true)
  addKeyListener(this)
  addMouseListener(new MouseAdapter {
    override def mousePressed(e: MouseEvent): Unit = {
      requestFocusInWindow()
      if (screen == "FLIGHT" && landReady &&
          e.getX >= 875 && e.getX <= 1165 &&
          e.getY >= 445 && e.getY <= 550) {
        startLanding()
      }
    }
  })

  val timer = new javax.swing.Timer(30, new ActionListener {
    override def actionPerformed(e: ActionEvent): Unit = gameTick()
  })
  timer.start()

  override def paintComponent(g: Graphics): Unit = {
    super.paintComponent(g)
    val gg = g.asInstanceOf[Graphics2D]
    gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
    drawHeader(gg)
    screen match {
      case "GARAGE" => drawGarage(gg)
      case "CITY" => drawCityView(gg)
      case "AIRPORT" => drawAirport(gg)
      case "FLIGHT" => drawFlight(gg)
      case "LANDING" => drawLanding(gg)
      case "BATTLE" => drawBattleView(gg)
      case "RESULT" => drawResult(gg)
      case "PROFILE" => drawProfile(gg)
      case _ => drawGarage(gg)
    }
    drawFooter(gg)
  }

  // ----------------------------------------------------------
  // HEADER / FOOTER
  // ----------------------------------------------------------
  def drawHeader(g: Graphics2D): Unit = {
    g.setColor(new Color(10, 13, 17))
    g.fillRect(0, 0, WIDTH, 68)

    g.setColor(GOLD)
    g.setFont(new Font("SansSerif", Font.BOLD, 22))
    g.drawString("DESI TRUCK BATTLE ROYALE", 20, 28)

    g.setColor(MUTED)
    g.setFont(new Font("SansSerif", Font.PLAIN, 12))
    g.drawString("PLAYER: " + currentUsername + "   ID: " + currentPlayerId, 20, 50)

    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 14))
    g.drawString("POINTS " + currentPoints, 865, 24)
    g.drawString("COINS " + currentCoins, 1005, 24)
    g.drawString("MATCHES " + totalMatches, 1135, 24)

    g.setColor(SAFE)
    g.drawString(screen, 865, 48)
  }

  def drawFooter(g: Graphics2D): Unit = {
    g.setColor(new Color(8, 11, 14, 235))
    g.fillRect(0, HEIGHT - 34, WIDTH, 34)
    g.setColor(MUTED)
    g.setFont(new Font("SansSerif", Font.PLAIN, 11))
    g.drawString(
      "ENTER continue   P profile   M local 2P   ESC back/logout   Original Java2D/Swing project",
      18,
      HEIGHT - 13
    )

    if (messageLife > 0) {
      g.setColor(GOLD)
      g.setFont(new Font("SansSerif", Font.BOLD, 13))
      g.drawString(message, 430, HEIGHT - 13)
    }
  }

  // ----------------------------------------------------------
  // GARAGE
  // ----------------------------------------------------------
  def drawGarage(g: Graphics2D): Unit = {
    g.setColor(BG)
    g.fillRect(0, 68, WIDTH, HEIGHT - 68)

    g.setColor(PANEL)
    g.fillRoundRect(45, 105, 1190, 535, 20, 20)

    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 29))
    g.drawString("INDIAN TRUCK GARAGE", 80, 150)

    g.setColor(MUTED)
    g.setFont(new Font("SansSerif", Font.PLAIN, 15))
    g.drawString("Start from the road. Your destination is the airport, then the sky, then the battle arena.", 80, 178)

    drawBigTruck(g, 180, 300)

    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 18))
    g.drawString("MISSION", 690, 245)
    val steps = Array(
      "1  ENTER CITY ROAD",
      "2  DRIVE TO AIRPORT",
      "3  ENTER AIRPLANE",
      "4  FLY UNTIL LAND IS READY",
      "5  PRESS L TO LAND",
      "6  LOOT HOUSES",
      "7  SURVIVE ENEMIES",
      "8  SCORE + SAVE"
    )
    g.setFont(new Font("SansSerif", Font.PLAIN, 16))
    var yy = 280
    var i = 0
    while (i < steps.length) {
      g.setColor(if (i < 3) SAFE else TEXT)
      g.drawString(steps(i), 690, yy)
      yy += 37
      i += 1
    }

    g.setColor(GOLD)
    g.setFont(new Font("SansSerif", Font.BOLD, 20))
    g.drawString("PRESS ENTER TO START", 390, 595)

    g.setColor(MUTED)
    g.setFont(new Font("SansSerif", Font.PLAIN, 13))
    g.drawString("M toggles local two-player mode", 390, 620)
  }

  def drawBigTruck(g: Graphics2D, x: Int, y: Int): Unit = {
    g.setColor(new Color(12, 15, 18))
    g.fillOval(x - 130, y + 130, 390, 38)

    g.setColor(TRUCK_DARK)
    g.fillRoundRect(x, y, 350, 165, 30, 30)
    g.setColor(TRUCK_RED)
    g.fillRoundRect(x + 15, y + 16, 220, 133, 22, 22)
    g.setColor(new Color(86, 147, 198))
    g.fillRoundRect(x + 246, y + 30, 80, 85, 12, 12)
    g.setColor(new Color(200, 230, 245))
    g.fillRect(x + 259, y + 42, 52, 19)
    g.fillRect(x + 259, y + 70, 52, 19)

    g.setColor(new Color(235, 184, 63))
    g.fillRect(x + 33, y + 62, 168, 38)
    g.setColor(new Color(95, 42, 31))
    g.setFont(new Font("SansSerif", Font.BOLD, 16))
    g.drawString("BHARAT CARGO", x + 54, y + 87)

    g.setColor(Color.BLACK)
    g.fillOval(x + 35, y + 130, 64, 64)
    g.fillOval(x + 258, y + 130, 64, 64)
    g.setColor(new Color(148, 148, 148))
    g.fillOval(x + 51, y + 146, 32, 32)
    g.fillOval(x + 274, y + 146, 32, 32)

    g.setColor(Color.WHITE)
    g.setFont(new Font("SansSerif", Font.BOLD, 12))
    g.drawString("MH ? ROAD KING", x + 119, y + 132)
  }

  // ----------------------------------------------------------
  // CITY
  // ----------------------------------------------------------
  def drawCityView(g: Graphics2D): Unit = {
    // ==========================================================
    // FULL 3D-STYLE THIRD-PERSON ROAD VIEW
    // Java2D pseudo-3D: perspective road, depth-scaled buildings,
    // layered skyline, traffic and a large detailed Indian truck.
    // ==========================================================
    drawSkyAndHorizon(g)

    val horizon = 245

    // Distant city silhouettes
    var city = 0
    while (city < 18) {
      val bx = 18 + city * 76
      val bh = 35 + (city * 17) % 85
      val bw = 52 + (city * 11) % 45
      g.setColor(
        if (city % 3 == 0) new Color(82, 94, 107)
        else if (city % 3 == 1) new Color(94, 103, 111)
        else new Color(74, 88, 98)
      )
      g.fillRect(bx, horizon - bh, bw, bh)
      g.setColor(new Color(145, 174, 190))
      var wy = horizon - bh + 10
      while (wy < horizon - 8) {
        var wx = bx + 8
        while (wx < bx + bw - 7) {
          g.fillRect(wx, wy, 5, 7)
          wx += 15
        }
        wy += 16
      }
      city += 1
    }

    // Ground
    g.setColor(new Color(105, 146, 79))
    g.fillRect(0, horizon, WIDTH, HEIGHT - horizon)

    // Far horizon haze
    g.setColor(new Color(218, 224, 204, 120))
    g.fillRect(0, horizon - 22, WIDTH, 30)

    // Main perspective road
    val vanX = WIDTH / 2
    val vanY = horizon + 12
    val roadBottomLeft = 70
    val roadBottomRight = WIDTH - 70
    val roadTopLeft = vanX - 55
    val roadTopRight = vanX + 55
    val roadPoly = new Polygon()
    roadPoly.addPoint(roadTopLeft, vanY)
    roadPoly.addPoint(roadTopRight, vanY)
    roadPoly.addPoint(roadBottomRight, HEIGHT)
    roadPoly.addPoint(roadBottomLeft, HEIGHT)
    g.setColor(new Color(58, 60, 64))
    g.fillPolygon(roadPoly)

    // Road shoulders
    g.setColor(new Color(190, 169, 113))
    val leftShoulder = new Polygon()
    leftShoulder.addPoint(roadTopLeft - 8, vanY)
    leftShoulder.addPoint(roadTopLeft, vanY)
    leftShoulder.addPoint(roadBottomLeft, HEIGHT)
    leftShoulder.addPoint(roadBottomLeft - 38, HEIGHT)
    g.fillPolygon(leftShoulder)

    val rightShoulder = new Polygon()
    rightShoulder.addPoint(roadTopRight, vanY)
    rightShoulder.addPoint(roadTopRight + 8, vanY)
    rightShoulder.addPoint(roadBottomRight + 38, HEIGHT)
    rightShoulder.addPoint(roadBottomRight, HEIGHT)
    g.fillPolygon(rightShoulder)

    // Perspective lane markings
    g.setColor(ROAD_MARK)
    var d = 0
    while (d < 12) {
      val t = d / 12.0
      val y1 = (vanY + t * t * (HEIGHT - vanY)).toInt
      val t2 = (d + 0.52) / 12.0
      val y2 = (vanY + Math.min(1.0, t2) * Math.min(1.0, t2) * (HEIGHT - vanY)).toInt
      val half1 = (4 + t * 15).toInt
      val half2 = (4 + Math.min(1.0, t2) * 15).toInt
      val lanePoly = new Polygon()
      lanePoly.addPoint(vanX - half1, y1)
      lanePoly.addPoint(vanX + half1, y1)
      lanePoly.addPoint(vanX + half2, y2)
      lanePoly.addPoint(vanX - half2, y2)
      g.fillPolygon(lanePoly)
      d += 1
    }

    // Side poles / trees for depth
    var sideObj = 0
    while (sideObj < 20) {
      val z = 0.15 + sideObj / 20.0
      val sy = (vanY + z * z * (HEIGHT - vanY)).toInt
      val sxL = (vanX - (70 + z * 540)).toInt
      val sxR = (vanX + (70 + z * 540)).toInt
      val hh = (18 + z * 105).toInt
      drawRoadsideTree(g, sxL, sy, hh)
      drawRoadsideTree(g, sxR, sy, hh)
      sideObj += 1
    }

    // Project nearby buildings into perspective from world coordinates
    var b = 0
    while (b < buildings.length) {
      val q = buildings(b)
      drawPerspectiveBuilding(g, q)
      b += 1
    }

    // Traffic projected from world coordinates
    var tr = 0
    while (tr < traffic.length) {
      val q = traffic(tr)
      drawPerspectiveTraffic(g, q)
      tr += 1
    }

    // Airport billboard / waypoint
    val ap = projectRelative(1080.0, 970.0)
    if (ap._3 > 0) {
      val ax = ap._1
      val ay = ap._2
      val ah = Math.max(35, Math.min(150, (5600.0 / ap._3).toInt))
      g.setColor(new Color(245, 248, 250, 235))
      g.fillRoundRect(ax - 70, ay - ah, 140, 48, 12, 12)
      g.setColor(GOLD)
      g.fillOval(ax - 14, ay - ah - 28, 28, 28)
      g.setColor(Color.BLACK)
      g.setFont(new Font("SansSerif", Font.BOLD, 12))
      g.drawString("A", ax - 5, ay - ah - 8)
      g.setColor(new Color(48, 56, 65))
      g.drawString("DESI AIRPORT", ax - 45, ay - ah + 28)
      g.setColor(SAFE)
      g.setFont(new Font("SansSerif", Font.BOLD, 13))
      g.drawString("DIST " + ap._3.toInt + "m", ax - 43, ay + 22)
    }

    // Detailed player truck with ground shadow, lights and cab driver
    draw3DTruck(g, WIDTH / 2, HEIGHT - 135, truckAngle, Math.max(0.88, Math.min(1.22, 0.92 + Math.abs(truckSpeed) * 0.035)))

    // Cockpit-style HUD
    drawBar(g, 22, 92, 260, 18, truckFuel, 100, "FUEL")
    drawBar(g, 22, 121, 260, 18, Math.abs(truckSpeed) * 14, 100, "SPEED")
    drawInfoBox(g, 960, 88, 290, 180, "3D ROAD HUD", Array(
      "W / UP = accelerate",
      "S / DOWN = reverse",
      "A / D = steer",
      "R = refuel / restart route",
      "E = enter airport",
      "M = local 2P",
      "Camera: THIRD PERSON 3D"
    ))

    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 14))
    g.drawString("BHARAT NAGAR HIGHWAY", 24, 176)
    g.setFont(new Font("SansSerif", Font.PLAIN, 12))
    g.setColor(MUTED)
    g.drawString("Perspective depth ? buildings ? traffic ? road markers ? Indian roadside details", 24, 198)

    drawMiniMap(g)

    if (crashTicks > 0) {
      g.setColor(new Color(10, 12, 16, 225))
      g.fillRect(0, 68, WIDTH, HEIGHT - 68)
      g.setColor(ENEMY)
      g.setFont(new Font("SansSerif", Font.BOLD, 43))
      g.drawString("TRUCK ACCIDENT", 454, 305)
      g.setColor(TEXT)
      g.setFont(new Font("SansSerif", Font.PLAIN, 20))
      g.drawString("Collision detected ? automatic route restart", 410, 346)
    }
  }

  def drawSkyAndHorizon(g: Graphics2D): Unit = {
    var y = 68
    while (y < 250) {
      val q = (y - 68).toDouble / 182.0
      val r = (75 * (1 - q) + 185 * q).toInt
      val gg = (145 * (1 - q) + 218 * q).toInt
      val b = (210 * (1 - q) + 238 * q).toInt
      g.setColor(new Color(r, gg, b))
      g.fillRect(0, y, WIDTH, 1)
      y += 1
    }

    g.setColor(new Color(252, 249, 211))
    g.fillOval(1070, 98, 75, 75)

    // soft clouds
    var c = 0
    while (c < 8) {
      val cx = 40 + c * 165
      val cy = 105 + (c % 3) * 38
      g.setColor(new Color(255, 255, 255, 125))
      g.fillOval(cx, cy, 82, 28)
      g.fillOval(cx + 25, cy - 15, 78, 48)
      c += 1
    }
  }

  def projectRelative(wx: Double, wy: Double): (Int, Int, Double) = {
    val dx = wx - truckX
    val dy = wy - truckY
    val fwdX = Math.cos(truckAngle)
    val fwdY = Math.sin(truckAngle)
    val rightX = -fwdY
    val rightY = fwdX
    val depth = dx * fwdX + dy * fwdY
    val side = dx * rightX + dy * rightY
    if (depth <= 35) (0, 0, depth)
    else {
      val focal = 430.0
      val sx = WIDTH / 2.0 + side * focal / depth
      val sy = 250.0 + 2550.0 / depth
      (sx.toInt, sy.toInt, depth)
    }
  }

  def drawPerspectiveBuilding(g: Graphics2D, b: Building): Unit = {
    val p = projectRelative(b.x + b.w / 2, b.y + b.h)
    if (p._3 > 40 && p._3 < 1250 && p._1 > -180 && p._1 < WIDTH + 180) {
      val scale = Math.max(0.08, Math.min(1.7, 360.0 / p._3))
      val ww = Math.max(18, (b.w * 0.42 * scale).toInt)
      val hh = Math.max(25, (b.h * 1.0 * scale * (0.65 + b.floors * 0.06)).toInt)
      val baseY = p._2
      val x = p._1 - ww / 2
      val y = baseY - hh
      val depth = Math.max(6, (ww * 0.16).toInt)

      g.setColor(new Color(88, 75, 65))
      val side = new Polygon()
      side.addPoint(x + ww, y + 8)
      side.addPoint(x + ww + depth, y)
      side.addPoint(x + ww + depth, baseY - 5)
      side.addPoint(x + ww, baseY)
      g.fillPolygon(side)

      g.setColor(if (b.floors > 5) new Color(137, 124, 111) else new Color(173, 143, 106))
      g.fillRoundRect(x, y, ww, hh, 8, 8)

      val roof = new Polygon()
      roof.addPoint(x - 4, y + 8)
      roof.addPoint(x + ww / 2, y - Math.max(8, (12 * scale).toInt))
      roof.addPoint(x + ww + 4, y + 8)
      g.setColor(if (b.floors > 5) new Color(64, 66, 73) else new Color(106, 69, 54))
      g.fillPolygon(roof)

      val floorsVisible = Math.max(2, Math.min(7, b.floors))
      var row = 0
      while (row < floorsVisible && ww > 30) {
        var wx = x + 6
        val wy = y + 10 + row * Math.max(9, hh / floorsVisible)
        while (wx < x + ww - 8) {
          g.setColor(if ((row + wx) % 2 == 0) new Color(82, 153, 194) else new Color(63, 128, 168))
          g.fillRect(wx, wy, Math.max(3, (7 * scale).toInt), Math.max(4, (7 * scale).toInt))
          wx += Math.max(12, (18 * scale).toInt)
        }
        row += 1
      }

      if (scale > 0.25) {
        g.setColor(new Color(242, 236, 218))
        g.setFont(new Font("SansSerif", Font.BOLD, 9))
        g.drawString(b.name, x + 4, y + 12)
      }
    }
  }

  def drawPerspectiveTraffic(g: Graphics2D, q: Traffic): Unit = {
    val p = projectRelative(q.x, q.y)
    if (p._3 > 30 && p._3 < 1000 && p._1 > -80 && p._1 < WIDTH + 80) {
      val scale = Math.max(0.12, Math.min(1.25, 280.0 / p._3))
      val w = Math.max(9, (42 * scale).toInt)
      val h = Math.max(6, (22 * scale).toInt)
      val x = p._1 - w / 2
      val y = p._2 - h
      g.setColor(new Color(12, 14, 18, 110))
      g.fillOval(x - 3, y + h - 1, w + 8, Math.max(4, h / 3))
      g.setColor(
        if (q.kind == "BUS") new Color(238, 178, 53)
        else if (q.kind == "AUTO") new Color(58, 150, 72)
        else new Color(70, 95, 161)
      )
      g.fillRoundRect(x, y, w, h, 5, 5)
      g.setColor(new Color(195, 225, 238))
      g.fillRect(x + Math.max(2, w / 7), y + 2, Math.max(3, w - w / 4), Math.max(2, h / 3))
    }
  }

  def drawRoadsideTree(g: Graphics2D, x: Int, y: Int, h: Int): Unit = {
    val trunkW = Math.max(3, h / 9)
    g.setColor(new Color(100, 69, 45))
    g.fillRect(x - trunkW / 2, y - h / 3, trunkW, h / 3)
    g.setColor(new Color(47, 122, 64, 235))
    g.fillOval(x - h / 4, y - h, h / 2, h / 2)
    g.fillOval(x - h / 7, y - h - h / 5, h / 2, h / 2)
    g.fillOval(x - h / 2, y - h / 2, h / 2, h / 2)
  }

  def worldToScreenX(x: Double): Int = ((x / WORLD_W) * WIDTH).toInt
  def cityWorldY(y: Double): Int = 68 + ((y / WORLD_H) * (HEIGHT - 68)).toInt

  def drawBuilding(g: Graphics2D, b: Building): Unit = {
    val x = worldToScreenX(b.x)
    val y = cityWorldY(b.y)
    val w = Math.max(72, worldToScreenX(b.x + b.w) - x)
    val h = Math.max(58, cityWorldY(b.y + b.h) - y)
    val depth = Math.max(10, w / 12)

    // pseudo-3D side
    g.setColor(new Color(105, 91, 76))
    val side = new Polygon()
    side.addPoint(x + w, y + 8)
    side.addPoint(x + w + depth, y)
    side.addPoint(x + w + depth, y + h - 8)
    side.addPoint(x + w, y + h)
    g.fillPolygon(side)

    g.setColor(if (b.floors % 2 == 0) HOUSE else new Color(160, 141, 113))
    g.fillRoundRect(x, y, w, h, 9, 9)

    val roof = new Polygon()
    roof.addPoint(x - 4, y + 8)
    roof.addPoint(x + w / 2, y - 20)
    roof.addPoint(x + w + 4, y + 8)
    g.setColor(if (b.floors > 5) new Color(70, 68, 76) else ROOF)
    g.fillPolygon(roof)

    // windows on every level
    val levels = Math.max(2, Math.min(7, b.floors))
    var row = 0
    while (row < levels) {
      var xx = x + 12
      val yy = y + 23 + row * Math.max(18, (h - 35) / levels)
      while (xx < x + w - 15) {
        g.setColor(if ((row + xx) % 2 == 0) new Color(83, 149, 188) else new Color(67, 124, 165))
        g.fillRect(xx, yy, 9, 10)
        xx += 23
      }
      row += 1
    }

    // doors / shopfronts
    g.setColor(new Color(72, 54, 42))
    g.fillRect(x + w / 2 - 10, y + h - 28, 20, 28)
    g.setColor(Color.WHITE)
    g.setFont(new Font("SansSerif", Font.BOLD, 8))
    g.drawString(b.name, x + 5, y + h - 6)
  }

  def drawTraffic(g: Graphics2D, x: Int, y: Int, kind: String): Unit = {
    val w = if (kind == "BUS") 54 else if (kind == "AUTO") 34 else 42
    val h = if (kind == "BUS") 25 else 20
    g.setColor(
      kind match {
        case "BUS" => new Color(238, 178, 53)
        case "AUTO" => new Color(54, 146, 73)
        case _ => new Color(73, 95, 160)
      }
    )
    g.fillRoundRect(x, y, w, h, 7, 7)
    g.setColor(new Color(195, 225, 238))
    g.fillRect(x + 8, y + 4, Math.max(10, w - 16), 7)
    g.setColor(Color.BLACK)
    g.fillOval(x + 5, y + h - 5, 8, 8)
    g.fillOval(x + w - 13, y + h - 5, 8, 8)
  }

  def drawTruck(g: Graphics2D, x: Int, y: Int, angle: Double): Unit = {
    draw3DTruck(g, x, y, angle, 1.0)
  }

  def draw3DTruck(g: Graphics2D, x: Int, y: Int, angle: Double, scale: Double): Unit = {
    val old = g.getTransform
    g.translate(x, y)
    g.rotate(angle)
    g.scale(scale, scale)

    // Ground shadow
    g.setColor(new Color(5, 7, 9, 150))
    g.fillOval(-155, 46, 310, 58)
    g.setColor(new Color(30, 32, 35, 120))
    g.fillOval(-115, 60, 230, 24)

    // Rear cargo box: depth layers
    g.setColor(new Color(100, 27, 23))
    g.fillRoundRect(-120, -104, 144, 112, 18, 18)
    g.setColor(new Color(170, 45, 37))
    g.fillRoundRect(-113, -100, 135, 104, 16, 16)
    g.setColor(new Color(224, 71, 53))
    g.fillRoundRect(-103, -94, 117, 92, 14, 14)

    // Cargo ribs
    g.setColor(new Color(128, 35, 30))
    var rib = -90
    while (rib <= 0) {
      g.fillRect(rib, -89, 6, 82)
      rib += 23
    }

    // Decorative Indian stripes
    g.setColor(new Color(242, 191, 55))
    g.fillRect(-102, -55, 112, 12)
    g.setColor(new Color(77, 145, 82))
    g.fillRect(-102, -42, 112, 7)

    // Rear cargo sign
    g.setColor(new Color(244, 224, 174))
    g.fillRoundRect(-82, -31, 83, 25, 8, 8)
    g.setColor(new Color(74, 43, 34))
    g.setFont(new Font("SansSerif", Font.BOLD, 13))
    g.drawString("BHARAT", -66, -14)

    // Chassis
    g.setColor(new Color(57, 55, 52))
    g.fillRoundRect(-135, -5, 210, 32, 12, 12)
    g.setColor(new Color(32, 34, 36))
    g.fillRect(-135, 13, 210, 9)

    // Cab with 3D front and side
    g.setColor(new Color(111, 31, 27))
    g.fillRoundRect(12, -75, 105, 101, 24, 24)
    g.setColor(new Color(195, 52, 42))
    g.fillRoundRect(19, -70, 92, 93, 20, 20)

    // windshield
    g.setColor(new Color(54, 119, 157))
    g.fillRoundRect(38, -61, 58, 45, 11, 11)
    g.setColor(new Color(188, 225, 240))
    g.fillRoundRect(44, -55, 46, 31, 7, 7)
    g.setColor(new Color(104, 160, 189, 140))
    g.fillRect(66, -56, 5, 32)

    // Hood / front face
    g.setColor(new Color(151, 37, 31))
    g.fillRoundRect(74, -11, 50, 37, 10, 10)
    g.setColor(new Color(76, 82, 87))
    g.fillRect(84, -2, 30, 12)

    // Driver inside cab
    g.setColor(new Color(35, 35, 37))
    g.fillOval(56, -43, 25, 25)
    g.setColor(new Color(199, 143, 102))
    g.fillOval(61, -45, 15, 15)
    g.setColor(new Color(40, 72, 102))
    g.fillRoundRect(57, -31, 24, 27, 7, 7)
    g.setColor(new Color(22, 25, 29))
    g.fillOval(52, -42, 31, 8)

    // Mirrors
    g.setColor(new Color(32, 35, 38))
    g.fillRoundRect(104, -65, 22, 8, 4, 4)
    g.fillRoundRect(104, 20, 22, 8, 4, 4)
    g.setColor(new Color(100, 150, 173))
    g.fillRect(111, -63, 10, 5)
    g.fillRect(111, 22, 10, 5)

    // Wheels and hubs
    val wheelX = Array(-70, 54)
    var wi = 0
    while (wi < wheelX.length) {
      g.setColor(Color.BLACK)
      g.fillOval(wheelX(wi), -10, 46, 46)
      g.setColor(new Color(52, 54, 57))
      g.fillOval(wheelX(wi) + 6, -4, 34, 34)
      g.setColor(new Color(178, 181, 184))
      g.fillOval(wheelX(wi) + 15, 5, 16, 16)
      g.setColor(new Color(111, 114, 118))
      g.fillOval(wheelX(wi) + 19, 9, 8, 8)
      wi += 1
    }

    // Headlights
    g.setColor(new Color(255, 238, 160))
    g.fillOval(110, -3, 11, 11)
    g.fillOval(110, 13, 11, 11)
    g.setColor(new Color(255, 251, 210, 120))
    g.fillOval(116, -7, 23, 19)
    g.fillOval(116, 9, 23, 19)

    // Front grill + plate
    g.setColor(new Color(35, 39, 42))
    g.fillRoundRect(91, 3, 31, 18, 4, 4)
    g.setColor(new Color(190, 195, 198))
    var grillY = 7
    while (grillY < 18) {
      g.fillRect(95, grillY, 23, 2)
      grillY += 5
    }
    g.setColor(Color.WHITE)
    g.fillRoundRect(89, 24, 33, 9, 3, 3)
    g.setColor(Color.BLACK)
    g.setFont(new Font("SansSerif", Font.BOLD, 7))
    g.drawString("MH 12", 93, 31)

    // Bumper + side steps
    g.setColor(new Color(78, 81, 84))
    g.fillRoundRect(104, 28, 26, 9, 4, 4)
    g.fillRoundRect(0, 22, 24, 8, 4, 4)

    // Decorative name
    g.setColor(new Color(245, 192, 55))
    g.setFont(new Font("SansSerif", Font.BOLD, 11))
    g.drawString("ROAD KING", -77, -71)

    g.setTransform(old)
  }

  def drawMiniMap(g: Graphics2D): Unit = {
    val x = 960
    val y = 500
    val w = 260
    val h = 155
    g.setColor(new Color(8, 11, 14, 225))
    g.fillRoundRect(x, y, w, h, 12, 12)
    g.setColor(new Color(92, 92, 98))
    var i = 0
    while (i < 4) {
      g.fillRect(x + 30 + i * 55, y + 10, 7, h - 20)
      i += 1
    }
    var j = 0
    while (j < 3) {
      g.fillRect(x + 10, y + 34 + j * 38, w - 20, 7)
      j += 1
    }
    g.setColor(TRUCK_RED)
    g.fillOval(x + (truckX * w / WORLD_W).toInt - 4, y + (truckY * h / WORLD_H).toInt - 4, 9, 9)
    g.setColor(GOLD)
    g.fillOval(x + (1080 * w / WORLD_W).toInt - 5, y + (970 * h / WORLD_H).toInt - 5, 11, 11)
    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 11))
    g.drawString("CITY MINI MAP", x + 12, y + h - 9)
  }

  // ----------------------------------------------------------
  // AIRPORT
  // ----------------------------------------------------------
  def drawAirport(g: Graphics2D): Unit = {
    g.setColor(new Color(100, 145, 159))
    g.fillRect(0, 68, WIDTH, HEIGHT - 68)
    g.setColor(new Color(77, 122, 78))
    g.fillRect(0, 68, WIDTH, 120)
    g.fillRect(0, 585, WIDTH, 175)

    g.setColor(new Color(65, 67, 72))
    g.fillRect(45, 220, 1180, 350)

    g.setColor(Color.WHITE)
    var x = 70
    while (x < 1200) {
      g.fillRect(x, 382, 92, 8)
      x += 140
    }

    // Terminal
    g.setColor(new Color(211, 218, 224))
    g.fillRoundRect(80, 105, 420, 96, 16, 16)
    g.setColor(new Color(74, 153, 196))
    g.fillRect(100, 132, 380, 20)
    g.setColor(new Color(48, 56, 65))
    g.setFont(new Font("SansSerif", Font.BOLD, 21))
    g.drawString("DESI INTERNATIONAL AIRPORT", 108, 181)

    // Hangar
    g.setColor(new Color(137, 146, 157))
    g.fillRoundRect(865, 105, 290, 112, 14, 14)
    g.setColor(GOLD)
    g.fillRect(890, 135, 240, 18)
    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 20))
    g.drawString("FLIGHT HANGAR", 925, 190)

    // Plane
    drawPlane(g, 625, 392, 0)

    // Airport service trucks
    var i = 0
    while (i < 7) {
      g.setColor(if (i % 2 == 0) new Color(232, 181, 48) else new Color(76, 148, 184))
      g.fillRoundRect(140 + i * 145, 603, 72, 35, 7, 7)
      g.setColor(Color.BLACK)
      g.fillOval(150 + i * 145, 630, 8, 8)
      g.fillOval(195 + i * 145, 630, 8, 8)
      i += 1
    }

    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 21))
    g.drawString("AIRPORT CONTROL", 55, 655)
    g.setColor(MUTED)
    g.setFont(new Font("SansSerif", Font.PLAIN, 15))
    g.drawString("E = enter aircraft", 55, 683)
    g.drawString("Your truck is parked outside TERMINAL 1", 55, 708)
    g.drawString("B = return to city", 55, 709)

    g.setColor(GOLD)
    g.setFont(new Font("SansSerif", Font.BOLD, 18))
    g.drawString("FLIGHT: BHARAT DROP ZONE", 820, 655)
    g.setColor(SAFE)
    g.drawString("Aircraft ready", 820, 683)
    g.setColor(GOLD)
    g.setFont(new Font("SansSerif", Font.BOLD, 15))
    g.drawString("STEP 1: DRIVE HERE  ->  STEP 2: PRESS E", 760, 710)
  }

  def drawPlane(g: Graphics2D, x: Int, y: Int, angle: Double): Unit = {
    val old = g.getTransform
    g.translate(x, y)
    g.rotate(angle)
    g.setColor(new Color(225, 230, 236))
    g.fillRoundRect(-105, -16, 210, 32, 18, 18)

    val wingA = new Polygon()
    wingA.addPoint(-15, -8)
    wingA.addPoint(-68, -74)
    wingA.addPoint(-1, -13)
    g.setColor(new Color(115, 131, 149))
    g.fillPolygon(wingA)

    val wingB = new Polygon()
    wingB.addPoint(-15, 8)
    wingB.addPoint(-68, 74)
    wingB.addPoint(-1, 13)
    g.fillPolygon(wingB)

    g.setColor(SKY)
    g.fillRoundRect(40, -10, 36, 20, 8, 8)
    g.setColor(GOLD)
    g.fillRect(-80, -4, 30, 8)

    // engine nacelles / 3D underside details
    g.setColor(new Color(91, 101, 113))
    g.fillOval(-25, -31, 28, 15)
    g.fillOval(-25, 16, 28, 15)
    g.setColor(new Color(42, 49, 57))
    g.fillOval(-18, -28, 15, 10)
    g.fillOval(-18, 18, 15, 10)
    g.setColor(new Color(248, 249, 250))
    g.fillRect(-91, -8, 22, 16)
    g.setColor(new Color(178, 184, 191))
    val tailPoly = new Polygon()
    tailPoly.addPoint(78, -5)
    tailPoly.addPoint(110, 0)
    tailPoly.addPoint(78, 5)
    g.fillPolygon(tailPoly)
    g.setTransform(old)
  }

  // ----------------------------------------------------------
  // FLIGHT
  // ----------------------------------------------------------
  def drawFlight(g: Graphics2D): Unit = {
    var y = 68
    while (y < HEIGHT) {
      val q = (y - 68).toDouble / (HEIGHT - 68).toDouble
      val r = (SKY.getRed * (1 - q) + 230 * q).toInt
      val gg = (SKY.getGreen * (1 - q) + 245 * q).toInt
      val b = (SKY.getBlue * (1 - q) + 250 * q).toInt
      g.setColor(new Color(r, gg, b))
      g.fillRect(0, y, WIDTH, 1)
      y += 1
    }

    var c = 0
    while (c < 13) {
      val cx = 30 + c * 100
      val cy = 150 + (c % 5) * 95
      g.setColor(CLOUD)
      g.fillOval(cx, cy, 80, 35)
      g.fillOval(cx + 25, cy - 18, 80, 55)
      g.fillOval(cx + 72, cy + 4, 68, 32)
      c += 1
    }

    g.setColor(new Color(86, 139, 77))
    val ground = new Polygon()
    ground.addPoint(0, 640)
    ground.addPoint(270, 615)
    ground.addPoint(620, 665)
    ground.addPoint(950, 615)
    ground.addPoint(WIDTH, 645)
    ground.addPoint(WIDTH, HEIGHT)
    ground.addPoint(0, HEIGHT)
    g.fillPolygon(ground)

    g.setColor(new Color(83, 83, 88))
    g.drawLine(0, 706, WIDTH, 626)
    g.drawLine(240, HEIGHT, 500, 645)
    g.drawLine(920, HEIGHT, 825, 635)

    drawPlane(g, 630, 410, 0)
    drawBar(g, 55, 105, 430, 24, planeProgress, 100, "FLIGHT PROGRESS")

    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 24))
    g.drawString(if (landReady) "LANDING IS READY" else "FLYING TO DROP ZONE", 55, 165)

    g.setColor(MUTED)
    g.setFont(new Font("SansSerif", Font.PLAIN, 14))
    g.drawString("W/S = altitude    A/D = course    SPACE = boost", 55, 194)
    g.drawString("Altitude: " + planeAltitude.toInt + " m", 55, 228)
    g.drawString("Drop Zone: BHARAT WILDERNESS", 55, 255)

    drawInfoBox(g, 900, 220, 300, 200, "AIRCRAFT SYSTEMS", Array(
      "Engine status: STABLE",
      "Fuel: 86%",
      "Weather: CLEAR",
      "Route: BHARAT EAST",
      "Landing gear: " + (if (landReady) "READY" else "LOCKED"),
      "Player: " + currentPlayerId
    ))

    if (landReady) {
      g.setColor(new Color(12, 28, 22, 235))
      g.fillRoundRect(875, 445, 290, 105, 18, 18)
      g.setColor(SAFE)
      g.setStroke(new BasicStroke(3f))
      g.drawRoundRect(875, 445, 290, 105, 18, 18)
      g.setFont(new Font("SansSerif", Font.BOLD, 28))
      g.drawString("LAND NOW", 955, 480)
      g.setFont(new Font("SansSerif", Font.BOLD, 16))
      g.drawString("L  or  ENTER", 968, 512)
      g.setFont(new Font("SansSerif", Font.PLAIN, 12))
      g.drawString("Aircraft ready for descent", 944, 535)
    }
  }

  // ----------------------------------------------------------
  // BATTLE VIEW
  // ----------------------------------------------------------
  def drawBattleView(g: Graphics2D): Unit = {
    g.setColor(GRASS)
    g.fillRect(0, 68, WIDTH, HEIGHT - 68)

    // ==========================================================
    // 3D-STYLE BATTLE TERRAIN LAYER
    // ==========================================================
    drawBattlePerspectiveGround(g)

    // Decorative terrain
    var p = 0
    while (p < 24) {
      val xx = (p * 157) % WIDTH
      val yy = 95 + ((p * 113) % 555)
      g.setColor(if (p % 2 == 0) GRASS2 else new Color(119, 164, 92))
      g.fillOval(xx, yy, 115, 68)
      p += 1
    }

    // Roads
    g.setColor(new Color(84, 84, 88))
    g.fillRect(0, 325, WIDTH, 42)
    g.fillRect(0, 560, WIDTH, 42)
    g.fillRect(375, 68, 44, HEIGHT)
    g.fillRect(830, 68, 44, HEIGHT)
    g.fillRect(1115, 68, 44, HEIGHT)

    g.setColor(GOLD)
    var rx = 0
    while (rx < WIDTH) {
      g.fillRect(rx, 344, 28, 4)
      g.fillRect(rx, 579, 28, 4)
      rx += 60
    }

    // Water pond
    g.setColor(WATER)
    g.fillOval(1180, 395, 290, 155)

    // Houses
    var h = 0
    while (h < 18) {
      val xx = 65 + ((h * 194) % 1030)
      val yy = 105 + ((h * 151) % 510)
      drawBattleHouse(g, xx, yy, 110 + (h % 3) * 15, 76 + (h % 2) * 15)
      h += 1
    }

    // Trees
    var t = 0
    while (t < 30) {
      val xx = 25 + ((t * 263) % 1200)
      val yy = 90 + ((t * 173) % 570)
      drawTree(g, xx, yy)
      t += 1
    }

    // Safe zone
    val zonePulse = (tickNo % 80).toInt
    val zoneR = 250 + zonePulse / 7
    g.setColor(new Color(75, 160, 235, 42))
    g.fillOval(640 - zoneR, 400 - zoneR, zoneR * 2, zoneR * 2)
    g.setColor(new Color(100, 180, 255, 170))
    g.setStroke(new BasicStroke(3f))
    g.drawOval(640 - zoneR, 400 - zoneR, zoneR * 2, zoneR * 2)

    // Loot
    var l = 0
    while (l < loot.length) {
      val q = loot(l)
      if (!q.taken) {
        val x = worldToBattleX(q.x)
        val y = worldToBattleY(q.y)
        g.setColor(q.kind match {
          case "AMMO" => GOLD
          case "MEDKIT" => SAFE
          case "ARMOR" => BLUE
          case "SMG" => new Color(191, 82, 192)
          case "RIFLE" => new Color(205, 100, 71)
          case "COINS" => new Color(239, 183, 46)
          case _ => TEXT
        })
        g.fillRoundRect(x - 7, y - 7, 14, 14, 4, 4)
      }
      l += 1
    }

    // Enemies
    var e = 0
    while (e < enemies.length) {
      val q = enemies(e)
      if (q.alive) {
        val x = worldToBattleX(q.x)
        val y = worldToBattleY(q.y)
        g.setColor(ENEMY)
        g.fillOval(x - 13, y - 13, 26, 26)
        g.setColor(Color.WHITE)
        g.fillOval(x - 6, y - 6, 4, 4)
        g.fillOval(x + 2, y - 6, 4, 4)
        g.setColor(Color.BLACK)
        g.fillRect(x - 17, y - 26, 34, 5)
        g.setColor(SAFE)
        g.fillRect(x - 17, y - 26, Math.max(0, q.hp) * 34 / 120, 5)
        g.setColor(TEXT)
        g.setFont(new Font("SansSerif", Font.PLAIN, 8))
        g.drawString(q.name, x - 18, y + 24)
      }
      e += 1
    }

    // Bullets
    var bi = 0
    while (bi < bullets.length) {
      val q = bullets(bi)
      if (q.alive) {
        g.setColor(if (q.owner == 1) GOLD else ENEMY)
        g.fillOval(worldToBattleX(q.x) - 3, worldToBattleY(q.y) - 3, 6, 6)
      }
      bi += 1
    }

    // Player 1
    val px = worldToBattleX(playerX)
    val py = worldToBattleY(playerY)
    g.setColor(FRIEND)
    g.fillOval(px - 15, py - 15, 30, 30)
    g.setColor(Color.WHITE)
    g.fillOval(px - 6, py - 7, 5, 5)
    g.fillOval(px + 2, py - 7, 5, 5)
    g.setColor(Color.BLACK)
    g.fillRect(px - 11, py + 7, 22, 3)

    // Player 2
    if (twoPlayer) {
      val p2sx = worldToBattleX(p2X)
      val p2sy = worldToBattleY(p2Y)
      g.setColor(PLAYER2)
      g.fillOval(p2sx - 14, p2sy - 14, 28, 28)
      g.setColor(TEXT)
      g.setFont(new Font("SansSerif", Font.BOLD, 10))
      g.drawString("P2", p2sx - 8, p2sy - 20)
    }

    drawBattleHUD(g)

    if (hp <= 0) {
      g.setColor(new Color(7, 10, 13, 205))
      g.fillRect(0, 68, WIDTH, HEIGHT - 68)
      g.setColor(TEXT)
      g.setFont(new Font("SansSerif", Font.BOLD, 42))
      g.drawString("MATCH OVER", 470, 305)
      g.setFont(new Font("SansSerif", Font.PLAIN, 20))
      g.drawString("Press ENTER for results", 485, 350)
    }
  }

  def worldToBattleX(x: Double): Int = ((x / WORLD_W) * WIDTH).toInt
  def worldToBattleY(y: Double): Int = 68 + ((y / WORLD_H) * (HEIGHT - 68)).toInt

  def drawBattlePerspectiveGround(g: Graphics2D): Unit = {
    val horizon = 195
    var y = horizon
    while (y < HEIGHT) {
      val q = (y - horizon).toDouble / (HEIGHT - horizon).toDouble
      val shade = (98 + q * 38).toInt
      g.setColor(new Color(Math.min(160, shade), Math.min(175, shade + 22), Math.min(120, shade - 4)))
      g.fillRect(0, y, WIDTH, 1)
      y += 1
    }

    val vanX = WIDTH / 2
    g.setColor(new Color(71, 73, 76))
    val main = new Polygon()
    main.addPoint(vanX - 36, horizon + 20)
    main.addPoint(vanX + 36, horizon + 20)
    main.addPoint(WIDTH - 80, HEIGHT)
    main.addPoint(80, HEIGHT)
    g.fillPolygon(main)

    g.setColor(new Color(193, 165, 104))
    val shL = new Polygon()
    shL.addPoint(vanX - 42, horizon + 20)
    shL.addPoint(vanX - 36, horizon + 20)
    shL.addPoint(80, HEIGHT)
    shL.addPoint(35, HEIGHT)
    g.fillPolygon(shL)
    val shR = new Polygon()
    shR.addPoint(vanX + 36, horizon + 20)
    shR.addPoint(vanX + 42, horizon + 20)
    shR.addPoint(WIDTH - 35, HEIGHT)
    shR.addPoint(WIDTH - 80, HEIGHT)
    g.fillPolygon(shR)

    g.setColor(GOLD)
    var i = 0
    while (i < 9) {
      val t = i / 9.0
      val yy = (horizon + 25 + t * t * (HEIGHT - horizon - 30)).toInt
      val half = (3 + t * 26).toInt
      g.fillRect(vanX - half, yy, half * 2, Math.max(3, (5 + t * 4).toInt))
      i += 1
    }

    // distant skyline
    var s = 0
    while (s < 16) {
      val bw = 35 + (s * 13) % 70
      val bh = 30 + (s * 17) % 100
      val bx = s * 84
      g.setColor(if (s % 2 == 0) new Color(91, 98, 101) else new Color(78, 88, 91))
      g.fillRect(bx, horizon - bh, bw, bh)
      s += 1
    }
  }

  def drawBattleHouse(g: Graphics2D, x: Int, y: Int, w: Int, h: Int): Unit = {
    g.setColor(new Color(199, 171, 133))
    g.fillRect(x, y + 15, w, h)
    val roof = new Polygon()
    roof.addPoint(x - 7, y + 17)
    roof.addPoint(x + w / 2, y - 22)
    roof.addPoint(x + w + 7, y + 17)
    g.setColor(ROOF)
    g.fillPolygon(roof)
    g.setColor(new Color(79, 137, 177))
    g.fillRect(x + 13, y + 31, 20, 18)
    g.fillRect(x + w - 33, y + 31, 20, 18)
    g.setColor(new Color(83, 54, 38))
    g.fillRect(x + w / 2 - 13, y + h - 29, 26, 29)
  }

  def drawTree(g: Graphics2D, x: Int, y: Int): Unit = {
    g.setColor(new Color(103, 70, 45))
    g.fillRect(x - 4, y + 10, 8, 26)
    g.setColor(new Color(48, 123, 64))
    g.fillOval(x - 23, y - 18, 46, 45)
    g.fillOval(x - 6, y - 27, 48, 48)
    g.fillOval(x - 37, y - 3, 46, 42)
  }

  def drawBattleHUD(g: Graphics2D): Unit = {
    g.setColor(new Color(8, 11, 14, 230))
    g.fillRoundRect(18, 82, 330, 190, 14, 14)
    drawBar(g, 35, 107, 270, 18, hp, 100, "HP")
    drawBar(g, 35, 137, 270, 18, armor, 100, "ARMOR")
    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 16))
    g.drawString("GUN: " + gunNames(gun), 35, 187)
    g.drawString("AMMO: " + ammo + " / " + gunAmmo(gun), 35, 214)
    g.drawString("KILLS: " + currentKills + "   LOOT: " + lootCount, 35, 241)

    drawInfoBox(g, 1005, 85, 245, 235, "BATTLE CONTROLS", Array(
      "WASD = move",
      "SPACE = shoot",
      "1-4 = guns",
      "E = pickup",
      "R = reload",
      "F = medkit",
      "M = local 2P",
      "P = profile",
      "ENTER = result"
    ))

    g.setColor(GOLD)
    g.setFont(new Font("SansSerif", Font.BOLD, 16))
    g.drawString("POINTS: " + currentPoints, 1060, 350)
    g.setColor(MUTED)
    g.setFont(new Font("SansSerif", Font.PLAIN, 12))
    g.drawString("Enemies remaining: " + enemies.count(_.alive), 1020, 376)
  }

  // ----------------------------------------------------------
  // RESULT / PROFILE
  // ----------------------------------------------------------
  def drawResult(g: Graphics2D): Unit = {
    g.setColor(BG)
    g.fillRect(0, 68, WIDTH, HEIGHT - 68)
    g.setColor(GOLD)
    g.setFont(new Font("SansSerif", Font.BOLD, 42))
    g.drawString(if (gameWon) "VICTORY RESULT" else "MATCH RESULT", 415, 160)

    val rows = Array(
      ("PLAYER", currentUsername),
      ("PLAYER ID", currentPlayerId),
      ("KILLS", currentKills.toString),
      ("MATCHES", totalMatches.toString),
      ("TOTAL POINTS", currentPoints.toString),
      ("COINS", currentCoins.toString),
      ("BEST KILLS", bestKills.toString)
    )
    var y = 225
    var i = 0
    while (i < rows.length) {
      g.setColor(MUTED)
      g.setFont(new Font("SansSerif", Font.BOLD, 17))
      g.drawString(rows(i)._1, 300, y)
      g.setColor(TEXT)
      g.setFont(new Font("SansSerif", Font.PLAIN, 17))
      g.drawString(rows(i)._2, 570, y)
      y += 45
      i += 1
    }

    g.setColor(SAFE)
    g.setFont(new Font("SansSerif", Font.BOLD, 22))
    g.drawString("PROGRESS SAVED TO YOUR ACCOUNT", 370, 555)
    g.setColor(MUTED)
    g.setFont(new Font("SansSerif", Font.PLAIN, 15))
    g.drawString("ENTER = new journey     P = profile     ESC = garage", 420, 590)
  }

  def drawProfile(g: Graphics2D): Unit = {
    g.setColor(BG)
    g.fillRect(0, 68, WIDTH, HEIGHT - 68)
    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 35))
    g.drawString("PLAYER PROFILE", 465, 145)

    val rows = Array(
      ("USERNAME", currentUsername),
      ("PLAYER ID", currentPlayerId),
      ("TOTAL POINTS", currentPoints.toString),
      ("COINS", currentCoins.toString),
      ("MATCHES PLAYED", totalMatches.toString),
      ("BEST KILLS", bestKills.toString),
      ("CURRENT KILLS", currentKills.toString),
      ("LOCAL 2P", if (twoPlayer) "ON" else "OFF")
    )

    var y = 205
    var i = 0
    while (i < rows.length) {
      g.setColor(MUTED)
      g.setFont(new Font("SansSerif", Font.BOLD, 17))
      g.drawString(rows(i)._1, 305, y)
      g.setColor(TEXT)
      g.setFont(new Font("SansSerif", Font.PLAIN, 17))
      g.drawString(rows(i)._2, 600, y)
      y += 43
      i += 1
    }

    g.setColor(GOLD)
    g.setFont(new Font("SansSerif", Font.BOLD, 19))
    g.drawString("Your old points remain attached to the same saved Player ID.", 300, 575)
  }

  // ----------------------------------------------------------
  // UI HELPERS
  // ----------------------------------------------------------
  def drawBar(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, value: Double, max: Double, label: String): Unit = {
    g.setColor(new Color(8, 11, 14, 230))
    g.fillRoundRect(x, y, w, h, 8, 8)
    val ratio = Math.max(0.0, Math.min(1.0, value / max))
    g.setColor(if (ratio > 0.60) SAFE else if (ratio > 0.30) GOLD else ENEMY)
    g.fillRoundRect(x + 2, y + 2, ((w - 4) * ratio).toInt, h - 4, 7, 7)
    g.setColor(Color.WHITE)
    g.setFont(new Font("SansSerif", Font.BOLD, 10))
    g.drawString(label, x + 7, y + h - 5)
  }

  def drawInfoBox(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, title: String, lines: Array[String]): Unit = {
    g.setColor(new Color(8, 11, 14, 230))
    g.fillRoundRect(x, y, w, h, 12, 12)
    g.setColor(GOLD)
    g.setFont(new Font("SansSerif", Font.BOLD, 14))
    g.drawString(title, x + 14, y + 22)
    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.PLAIN, 12))
    var yy = y + 43
    var i = 0
    while (i < lines.length && yy < y + h - 7) {
      g.drawString(lines(i), x + 14, yy)
      yy += 20
      i += 1
    }
  }

  // ----------------------------------------------------------
  // LOOP
  // ----------------------------------------------------------
  def gameTick(): Unit = {
    val now = System.currentTimeMillis()
    val dt = Math.min(0.05, (now - lastTime) / 1000.0)
    lastTime = now
    tickNo += 1
    if (messageLife > 0) messageLife -= 1

    screen match {
      case "CITY" => updateCity(dt)
      case "FLIGHT" => updateFlight(dt)
      case "LANDING" =>
        landingTicks = Math.max(0, landingTicks - 1)
        if (landingTicks <= 0) {
          buildBattle()
          screen = "BATTLE"
          showMessage("LANDING COMPLETE - BATTLE STARTED")
        }
      case "BATTLE" => updateBattle(dt)
      case _ => ()
    }
    repaint()
  }

  def updateCity(dt: Double): Unit = {
    if (crashTicks > 0) {
      crashTicks -= 1
      if (crashTicks == 0) {
        truckX = 190
        truckY = 1060
        truckAngle = -Math.PI / 2.0
        truckSpeed = 0
        truckFuel = 100
        showMessage("TRUCK RESTARTED - drive carefully")
      }
      return
    }

    var move = 0.0
    if (held.contains("W") || held.contains("UP")) move += 1
    if (held.contains("S") || held.contains("DOWN")) move -= 0.6
    if (held.contains("A") || held.contains("LEFT")) truckAngle -= 0.05
    if (held.contains("D") || held.contains("RIGHT")) truckAngle += 0.05

    if (move != 0 && truckFuel > 0) {
      truckSpeed += move * 0.68
      truckSpeed = Math.max(-3.0, Math.min(7.2, truckSpeed))
      truckX += Math.cos(truckAngle) * truckSpeed * 3.0
      truckY += Math.sin(truckAngle) * truckSpeed * 3.0
      truckFuel -= Math.abs(truckSpeed) * 0.011
    }
    truckSpeed *= 0.93
    truckX = Math.max(25, Math.min(WORLD_W - 25, truckX))
    truckY = Math.max(25, Math.min(WORLD_H - 25, truckY))

    var i = 0
    while (i < traffic.length) {
      val q = traffic(i)
      val nx = q.x + q.dx * (0.8 + (i % 4) * 0.25)
      val fixed = if (nx > WORLD_W + 50) -40 else if (nx < -50) WORLD_W + 40 else nx
      traffic.update(i, q.copy(x = fixed))
      i += 1
    }

    // Accident / collision system
    if (Math.abs(truckSpeed) > 1.8) {
      var k = 0
      while (k < traffic.length) {
        val q = traffic(k)
        val dx = truckX - q.x
        val dy = truckY - q.y
        if (dx * dx + dy * dy < 42 * 42) {
          crashTicks = 45
          truckSpeed = 0
          showMessage("ACCIDENT! Truck damaged - restarting...")
          k = traffic.length
        } else k += 1
      }
    }

    val dx = truckX - 1080
    val dy = truckY - 970
    val dist = Math.sqrt(dx * dx + dy * dy)
    if (dist < 260) showMessage("AIRPORT REACHED - press E")
  }

  def updateFlight(dt: Double): Unit = {
    if (!landReady) {
      val boost = if (held.contains("SPACE")) 2.0 else 1.0
      planeProgress += dt * 20.0 * boost
      planeAltitude = 1200 + planeProgress * 38
      if (planeProgress >= 100) {
        planeProgress = 100
        landReady = true
        showMessage("LAND READY - press L or ENTER")
      }
    } else {
      // Keep showing a ready aircraft until the player chooses LAND.
      planeAltitude = 5000
    }
    if (held.contains("UP") || held.contains("W")) planeAltitude += 3
    if (held.contains("DOWN") || held.contains("S")) planeAltitude = Math.max(300, planeAltitude - 3)
  }

  def startLanding(): Unit = {
    if (screen == "FLIGHT" && landReady && landingTicks <= 0) {
      landingTicks = 75
      screen = "LANDING"
      showMessage("LANDING: aircraft descending...")
    }
  }

  def drawLanding(g: Graphics2D): Unit = {
    var y = 68
    while (y < HEIGHT) {
      val q = (y - 68).toDouble / (HEIGHT - 68).toDouble
      val rr = (95 * (1 - q) + 210 * q).toInt
      val gg = (165 * (1 - q) + 235 * q).toInt
      val bb = (220 * (1 - q) + 190 * q).toInt
      g.setColor(new Color(rr, gg, bb))
      g.fillRect(0, y, WIDTH, 1)
      y += 1
    }

    g.setColor(new Color(74, 121, 73))
    g.fillRect(0, 510, WIDTH, 250)
    g.setColor(new Color(73, 73, 78))
    g.fillRect(240, 555, 800, 90)
    g.setColor(Color.WHITE)
    var lx = 275
    while (lx < 980) {
      g.fillRect(lx, 595, 55, 6)
      lx += 95
    }

    val landingDone = Math.max(0, Math.min(75, 75 - landingTicks))
    val fall = Math.min(180, landingDone * 2)
    drawPlane(g, 640, 250 + fall, 0.0)

    g.setColor(Color.WHITE)
    g.setFont(new Font("SansSerif", Font.BOLD, 31))
    g.drawString("LANDING", 520, 135)
    g.setFont(new Font("SansSerif", Font.PLAIN, 17))
    g.drawString("Landing gear: DOWN", 490, 170)
    g.drawString("Runway: DESI DROP ZONE", 475, 200)
    g.drawString("Please wait...", 555, 235)

    drawBar(g, 400, 275, 480, 25, landingDone, 75, "LANDING PROGRESS")
  }

  def updateBattle(dt: Double): Unit = {
    if (!battleActive || hp <= 0) return
    battleTicks += Math.max(1, (dt * 60).toInt)

    var vx = 0.0
    var vy = 0.0
    if (held.contains("W") || held.contains("UP")) vy -= 1
    if (held.contains("S") || held.contains("DOWN")) vy += 1
    if (held.contains("A") || held.contains("LEFT")) vx -= 1
    if (held.contains("D") || held.contains("RIGHT")) vx += 1
    val len = Math.sqrt(vx * vx + vy * vy)
    if (len > 0.1) {
      playerX += vx / len * 4.7
      playerY += vy / len * 4.7
    }
    playerX = Math.max(35, Math.min(WORLD_W - 35, playerX))
    playerY = Math.max(35, Math.min(WORLD_H - 35, playerY))

    if (twoPlayer) updateP2()

    if (held.contains("SPACE") && ammo > 0 && tickNo % fireGap(gun) == 0) shoot(1)
    if (held.contains("U") && twoPlayer && tickNo % 10 == 0) shoot(2)
    if (tickNo % 15 == 0) updateEnemies()
    updateBullets()
    autoPickup()

    val zx = playerX - 900
    val zy = playerY - 620
    val zdist = Math.sqrt(zx * zx + zy * zy)
    val zoneR = Math.max(120, 340 - battleTicks / 160)
    if (zdist > zoneR && tickNo % 10 == 0) {
      hp = Math.max(0, hp - 1)
      showMessage("OUTSIDE SAFE ZONE")
    }

    if (enemies.count(_.alive) == 0) completeBattle(true)
    else if (battleTicks > 42000) completeBattle(true)
  }

  def updateP2(): Unit = {
    var vx = 0.0
    var vy = 0.0
    if (held.contains("I")) vy -= 1
    if (held.contains("K")) vy += 1
    if (held.contains("J")) vx -= 1
    if (held.contains("L")) vx += 1
    val len = Math.sqrt(vx * vx + vy * vy)
    if (len > 0.1) {
      p2X += vx / len * 4.0
      p2Y += vy / len * 4.0
    }
    p2X = Math.max(35, Math.min(WORLD_W - 35, p2X))
    p2Y = Math.max(35, Math.min(WORLD_H - 35, p2Y))
  }

  def updateEnemies(): Unit = {
    var i = 0
    while (i < enemies.length) {
      val e = enemies(i)
      if (e.alive) {
        val dx = playerX - e.x
        val dy = playerY - e.y
        val d = Math.sqrt(dx * dx + dy * dy)
        if (d > 60) {
          enemies.update(i, e.copy(
            x = e.x + dx / Math.max(1, d) * 1.25,
            y = e.y + dy / Math.max(1, d) * 1.25,
            cool = Math.max(0, e.cool - 1)
          ))
        } else if (e.cool <= 0) {
          val damage = 3 + rng.nextInt(6)
          hp = Math.max(0, hp - Math.max(1, damage - armor / 30))
          armor = Math.max(0, armor - 1)
          enemies.update(i, enemies(i).copy(cool = 18 + rng.nextInt(22)))
        }
        if (hp <= 0) showMessage("Your player is down")
      }
      i += 1
    }
  }

  def shoot(owner: Int): Unit = {
    if (owner == 1 && ammo <= 0) return
    val sx = if (owner == 1) playerX else p2X
    val sy = if (owner == 1) playerY else p2Y
    val target = enemies.filter(_.alive).sortBy { e =>
      val dx = e.x - sx
      val dy = e.y - sy
      dx * dx + dy * dy
    }.headOption
    val angle = target match {
      case Some(e) => Math.atan2(e.y - sy, e.x - sx)
      case None => playerAngle
    }
    bullets += Bullet(
      sx,
      sy,
      Math.cos(angle) * 15,
      Math.sin(angle) * 15,
      owner,
      true
    )
    if (owner == 1) {
      ammo -= 1
      shots += 1
    }
  }

  def updateBullets(): Unit = {
    var i = bullets.length - 1
    while (i >= 0) {
      val b = bullets(i)
      if (b.alive) {
        b.x += b.vx
        b.y += b.vy
        if (b.x < 0 || b.y < 0 || b.x > WORLD_W || b.y > WORLD_H) b.alive = false

        if (b.owner == 1 && b.alive) {
          var j = 0
          while (j < enemies.length) {
            val e = enemies(j)
            if (e.alive) {
              val dx = e.x - b.x
              val dy = e.y - b.y
              if (dx * dx + dy * dy < 22 * 22) {
                val next = e.hp - gunDamage(gun)
                hits += 1
                currentPoints += 10
                if (next <= 0) {
                  currentKills += 1
                  currentPoints += 100
                  currentCoins += 25
                  enemies.update(j, e.copy(hp = 0, alive = false))
                  showMessage("Enemy defeated +100 points")
                } else {
                  enemies.update(j, e.copy(hp = next))
                  showMessage("Hit +10 points")
                }
                b.alive = false
              }
            }
            j += 1
          }
        }
      }
      if (!b.alive) bullets.remove(i)
      i -= 1
    }
  }

  def autoPickup(): Unit = {
    var i = 0
    while (i < loot.length) {
      val q = loot(i)
      if (!q.taken) {
        val dx = q.x - playerX
        val dy = q.y - playerY
        if (dx * dx + dy * dy < 32 * 32 && held.contains("E")) {
          loot.update(i, q.copy(taken = true))
          lootCount += 1
          currentPoints += 5
          q.kind match {
            case "AMMO" => ammo = Math.min(gunAmmo(gun), ammo + q.amount)
            case "MEDKIT" => medkits += 1
            case "ARMOR" => armor = Math.min(100, armor + 22)
            case "SMG" => gun = 1; ammo = gunAmmo(gun)
            case "RIFLE" => gun = 2; ammo = gunAmmo(gun)
            case "COINS" => currentCoins += q.amount
            case _ => ()
          }
          showMessage(q.kind + " collected")
        }
      }
      i += 1
    }
  }

  def completeBattle(won: Boolean): Unit = {
    if (!battleActive) return
    battleActive = false
    gameWon = won
    totalMatches += 1
    bestKills = Math.max(bestKills, currentKills)
    val bonus = if (won) 500 else 100
    currentPoints += bonus
    currentCoins += currentKills * 15
    saveCurrent()
    screen = "RESULT"
    showMessage("Match result saved")
  }

  // ----------------------------------------------------------
  // KEYBOARD
  // ----------------------------------------------------------
  override def keyPressed(e: KeyEvent): Unit = {
    val c = e.getKeyCode
    c match {
      case KeyEvent.VK_W => held += "W"
      case KeyEvent.VK_A => held += "A"
      case KeyEvent.VK_S => held += "S"
      case KeyEvent.VK_D => held += "D"
      case KeyEvent.VK_UP => held += "UP"
      case KeyEvent.VK_DOWN => held += "DOWN"
      case KeyEvent.VK_LEFT => held += "LEFT"
      case KeyEvent.VK_RIGHT => held += "RIGHT"
      case KeyEvent.VK_SPACE => held += "SPACE"
      case KeyEvent.VK_I => held += "I"
      case KeyEvent.VK_J => held += "J"
      case KeyEvent.VK_K => held += "K"
      case KeyEvent.VK_L => held += "L"
      case KeyEvent.VK_U => held += "U"
      case KeyEvent.VK_E => held += "E"
      case KeyEvent.VK_ENTER =>
        screen match {
          case "GARAGE" =>
            screen = "CITY"
            showMessage("City journey started")
          case "RESULT" =>
            buildCity()
            screen = "GARAGE"
          case "FLIGHT" if landReady =>
            startLanding()
          case "LANDING" =>
            ()
          case "BATTLE" if hp <= 0 =>
            completeBattle(false)
          case _ => ()
        }
      case KeyEvent.VK_B =>
        if (screen == "AIRPORT") screen = "CITY"
      case KeyEvent.VK_F =>
        if (screen == "BATTLE" && medkits > 0 && hp > 0) {
          hp = Math.min(100, hp + 35)
          medkits -= 1
          showMessage("Medkit used")
        }
      case KeyEvent.VK_R =>
        if (screen == "CITY") {
          crashTicks = 0
          truckX = 190
          truckY = 1060
          truckAngle = -Math.PI / 2.0
          truckSpeed = 0
          truckFuel = 100
          showMessage("TRUCK ROUTE RESTARTED")
        } else if (screen == "BATTLE") {
          ammo = gunAmmo(gun)
          showMessage("Reloaded")
        }
      case KeyEvent.VK_M =>
        twoPlayer = !twoPlayer
        showMessage("Local 2-player: " + (if (twoPlayer) "ON" else "OFF"))
      case KeyEvent.VK_P =>
        screen = "PROFILE"
      case KeyEvent.VK_ESCAPE =>
        if (screen == "PROFILE" || screen == "RESULT") {
          screen = "GARAGE"
        } else {
          saveCurrent()
          screen = "LOGIN"
          buildLogin()
        }
      case KeyEvent.VK_1 | KeyEvent.VK_2 | KeyEvent.VK_3 | KeyEvent.VK_4 =>
        if (screen == "BATTLE") {
          val selected = c match {
            case KeyEvent.VK_1 => 0
            case KeyEvent.VK_2 => 1
            case KeyEvent.VK_3 => 2
            case _ => 3
          }
          if (selected == 0) {
            gun = 0
            ammo = gunAmmo(gun)
            showMessage("Pistol equipped")
          } else if (currentCoins >= gunCost(selected)) {
            currentCoins -= gunCost(selected)
            gun = selected
            ammo = gunAmmo(gun)
            showMessage(gunNames(gun) + " equipped")
          } else {
            showMessage("Not enough coins")
          }
        }
      case _ => ()
    }

    // LAND action is evaluated after the held-key match so L works reliably.
    if (c == KeyEvent.VK_L && screen == "FLIGHT" && landReady) {
      startLanding()
    }

    // Separate transition for airport E is handled after the main match.
    if (screen == "CITY" && c == KeyEvent.VK_E) {
      val dx = truckX - 1080
      val dy = truckY - 970
      if (Math.sqrt(dx * dx + dy * dy) < 260) {
        screen = "AIRPORT"
        showMessage("Airport reached")
      }
    }

    if (screen == "AIRPORT" && c == KeyEvent.VK_E) {
      planeProgress = 0
      planeAltitude = 1000
      landReady = false
      screen = "FLIGHT"
      showMessage("Takeoff successful")
    }

    if (screen == "PROFILE" && c == KeyEvent.VK_ESCAPE) screen = "GARAGE"
    repaint()
  }

  override def keyReleased(e: KeyEvent): Unit = {
    val c = e.getKeyCode
    c match {
      case KeyEvent.VK_W => held -= "W"
      case KeyEvent.VK_A => held -= "A"
      case KeyEvent.VK_S => held -= "S"
      case KeyEvent.VK_D => held -= "D"
      case KeyEvent.VK_UP => held -= "UP"
      case KeyEvent.VK_DOWN => held -= "DOWN"
      case KeyEvent.VK_LEFT => held -= "LEFT"
      case KeyEvent.VK_RIGHT => held -= "RIGHT"
      case KeyEvent.VK_SPACE => held -= "SPACE"
      case KeyEvent.VK_I => held -= "I"
      case KeyEvent.VK_J => held -= "J"
      case KeyEvent.VK_K => held -= "K"
      case KeyEvent.VK_L => held -= "L"
      case KeyEvent.VK_U => held -= "U"
      case KeyEvent.VK_E => held -= "E"
      case _ => ()
    }
  }

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

// ------------------------------------------------------------
// GAME VIEW SWITCH
// ------------------------------------------------------------
def showGame(): Unit = {
  frame.getContentPane.removeAll()
  panel = new GamePanel()
  frame.setContentPane(panel)
  frame.revalidate()
  frame.repaint()
  panel.requestFocusInWindow()
}

// ------------------------------------------------------------
// CLOSE HANDLER
// ------------------------------------------------------------
frame.addWindowListener(new WindowAdapter {
  override def windowClosing(e: WindowEvent): Unit = {
    saveCurrent()
    System.exit(0)
  }
})

// ------------------------------------------------------------
// START
// ------------------------------------------------------------
loadAccounts()
buildLogin()
frame.setVisible(true)
frame.toFront()
frame.requestFocus()
panel.requestFocusInWindow()

/*
====================================================================
DESI TRUCK BATTLE ROYALE - FEATURE INDEX
====================================================================
001  New account
002  Sign in
003  Player ID generation
004  Password hashing
005  Local persistence
006  Point persistence
007  Coin persistence
008  Match persistence
009  Best-kill persistence
010  Demo guest
011  Garage
012  Truck model
013  Cargo body
014  Cabin
015  Windows
016  Wheels
017  Plate text
018  City map
019  Indian road naming
020  Buildings
021  Building floors
022  Building windows
023  Roofs
024  Traffic cars
025  Bus
026  Auto-rickshaw style traffic
027  Truck steering
028  Truck acceleration
029  Truck braking
030  Truck fuel
031  Refuel mechanic
032  Airport marker
033  Airport terminal
034  Airport runway
035  Hangar
036  Service vehicles
037  Airplane model
038  Flight progress
039  Aircraft altitude
040  Flight controls
041  Landing-ready state
042  Landing transition
043  Battle map
044  Houses
045  House doors
046  House windows
047  Trees
048  Pond
049  Battle roads
050  Safe-zone ring
051  Dynamic safe-zone radius
052  Loot system
053  Ammo loot
054  Medkit loot
055  Armor loot
056  SMG loot
057  Rifle loot
058  Coin loot
059  Enemy bots
060  Enemy HP
061  Enemy names
062  Enemy chase
063  Enemy attack cooldown
064  Player HP
065  Player armor
066  Pistol
067  SMG
068  Rifle
069  Shotgun
070  Weapon switching
071  Weapon costs
072  Ammo counts
073  Reload
074  Shooting
075  Bullet objects
076  Bullet movement
077  Hit detection
078  Damage
079  Kill counter
080  Kill rewards
081  Loot points
082  Coin rewards
083  Victory bonus
084  Result screen
085  Profile screen
086  Same account reload
087  Local two-player
088  P1 controls
089  P2 controls
090  P2 movement
091  P2 shooting
092  Mission text
093  Status messages
094  HUD
095  Mini-map
096  Fuel bar
097  Health bar
098  Armor bar
099  Speed bar
100  Players remaining
101  Loot count
102  Current weapon
103  Profile ID
104  Total points
105  Total coins
106  Total matches
107  Best kills
108  Local-only multiplayer mode
109  Original Java2D visuals
110  No external game assets
111  No external network dependency
112  Single-file project
113  Keyboard-first controls
114  Game flow state machine
115  Account save format
116  Base64 text fields
117  SHA-256 password hash
118  Safe local save error handling
119  Window close save
120  Game timer
121  Low-allocation rendering approach
122  Traffic animation
123  Plane animation state
124  Message timer
125  Mission transitions
126  Garage reset
127  New match flow
128  Result persistence
129  Profile navigation
130  Logout flow
====================================================================
The battle gameplay is an original educational implementation inspired
by the general battle-royale genre. It does not reproduce proprietary
maps, characters, logos, weapon models, or other copyrighted assets.
====================================================================
*/


/*
============================================================
POLYGON FIX + 3D POLISH
============================================================
Graphics2D.fillPolygon requires a Polygon (or coordinate arrays),
so all perspective polygons in this file are now built explicitly
with java.awt.Polygon and addPoint. This removes the reported
Array[Point] -> Polygon type mismatch.

3D-style rendering retained:
- perspective road
- depth lane markings
- multi-storey buildings
- traffic depth
- detailed Indian cargo truck
- airplane body, wings, engines and tail
- battle arena depth and structures
============================================================
*/