Code Sketch


yoiiiiii
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 = {
    g.setColor(new Color(185, 174, 145))
    g.fillRect(0, 68, WIDTH, HEIGHT - 68)

    // Water strip
    g.setColor(WATER)
    g.fillRect(0, 610, WIDTH, 130)

    // Main vertical roads
    val vx = Array(120, 465, 820, 1160)
    var i = 0
    while (i < vx.length) {
      val x = worldToScreenX(vx(i))
      g.setColor(ROAD)
      g.fillRect(x, 68, 70, HEIGHT - 68)
      g.setColor(ROAD_MARK)
      var y = 85
      while (y < HEIGHT) {
        g.fillRect(x + 33, y, 4, 22)
        y += 42
      }
      i += 1
    }

    // Horizontal roads
    val hy = Array(280, 630, 1000)
    var j = 0
    while (j < hy.length) {
      val y = cityWorldY(hy(j))
      g.setColor(ROAD)
      g.fillRect(0, y, WIDTH, 70)
      g.setColor(ROAD_MARK)
      var x = 15
      while (x < WIDTH) {
        g.fillRect(x, y + 33, 28, 4)
        x += 57
      }
      j += 1
    }

    // Buildings
    var b = 0
    while (b < buildings.length) {
      val z = buildings(b)
      drawBuilding(g, z)
      b += 1
    }

    // Traffic
    var t = 0
    while (t < traffic.length) {
      val q = traffic(t)
      val sx = worldToScreenX(q.x)
      val sy = cityWorldY(q.y)
      drawTraffic(g, sx, sy, q.kind)
      t += 1
    }

    // Airport
    val airportX = worldToScreenX(1080)
    val airportY = cityWorldY(970)
    g.setColor(new Color(237, 241, 245))
    g.fillRoundRect(airportX - 65, airportY - 42, 165, 90, 14, 14)
    g.setColor(new Color(70, 87, 98))
    g.setFont(new Font("SansSerif", Font.BOLD, 13))
    g.drawString("DESI AIRPORT", airportX - 38, airportY + 3)
    g.setColor(GOLD)
    g.fillOval(airportX - 18, airportY - 25, 28, 28)
    g.setColor(Color.BLACK)
    g.drawString("A", airportX - 9, airportY - 5)

    // Player truck
    drawTruck(g, worldToScreenX(truckX), cityWorldY(truckY), truckAngle)

    // Mini-map and HUD
    drawMiniMap(g)
    drawBar(g, 22, 91, 250, 18, truckFuel, 100, "FUEL")
    drawBar(g, 22, 120, 250, 18, Math.abs(truckSpeed) * 14, 100, "SPEED")

    drawInfoBox(g, 945, 90, 300, 145, "CITY MISSION", Array(
      "Reach the airport marker",
      "W/S = drive",
      "A/D = steer",
      "R = refuel",
      "E = enter airport",
      "M = local 2P"
    ))

    g.setColor(TEXT)
    g.setFont(new Font("SansSerif", Font.BOLD, 13))
    g.drawString("CITY: BHARAT NAGAR ? AIRPORT ROAD", 22, 160)

    if (crashTicks > 0) {
      g.setColor(new Color(15, 15, 18, 215))
      g.fillRect(0, 68, WIDTH, HEIGHT - 68)
      g.setColor(ENEMY)
      g.setFont(new Font("SansSerif", Font.BOLD, 42))
      g.drawString("TRUCK ACCIDENT", 455, 300)
      g.setColor(TEXT)
      g.setFont(new Font("SansSerif", Font.PLAIN, 20))
      g.drawString("Vehicle damaged. Automatic restart in progress...", 365, 345)
    }
  }

  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 = {
    val old = g.getTransform
    g.translate(x, y)
    g.rotate(angle)

    // Shadow
    g.setColor(new Color(8, 10, 12, 125))
    g.fillOval(-42, 16, 86, 25)

    // 3D lower chassis
    g.setColor(new Color(84, 30, 24))
    g.fillRoundRect(-42, -22, 84, 44, 10, 10)
    g.setColor(TRUCK_RED)
    g.fillRoundRect(-37, -19, 58, 38, 9, 9)

    // Large cargo box
    g.setColor(new Color(178, 46, 38))
    g.fillRoundRect(-31, -48, 50, 37, 8, 8)
    g.setColor(new Color(220, 74, 57))
    g.fillRoundRect(-28, -45, 44, 31, 6, 6)

    // Indian painted stripe
    g.setColor(new Color(239, 188, 55))
    g.fillRect(-26, -31, 40, 7)
    g.setColor(new Color(71, 143, 84))
    g.fillRect(-26, -24, 40, 5)

    // Cab
    g.setColor(new Color(132, 40, 32))
    g.fillRoundRect(18, -19, 33, 38, 9, 9)
    g.setColor(new Color(74, 143, 193))
    g.fillRoundRect(25, -13, 20, 17, 5, 5)
    g.setColor(new Color(188, 224, 239))
    g.fillRoundRect(27, -11, 16, 13, 3, 3)

    // Driver seated in cab
    g.setColor(new Color(45, 38, 32))
    g.fillOval(31, -7, 9, 9)
    g.setColor(new Color(205, 147, 105))
    g.fillOval(32, -8, 7, 7)
    g.setColor(new Color(36, 72, 104))
    g.fillRoundRect(30, -1, 11, 12, 4, 4)

    // Wheels with hubs
    val wheelYs = Array(-25, 13)
    var i = 0
    while (i < wheelYs.length) {
      g.setColor(Color.BLACK)
      g.fillOval(-27, wheelYs(i), 17, 17)
      g.fillOval(28, wheelYs(i), 17, 17)
      g.setColor(new Color(151, 151, 157))
      g.fillOval(-22, wheelYs(i) + 5, 7, 7)
      g.fillOval(33, wheelYs(i) + 5, 7, 7)
      i += 1
    }

    // Headlights and number plate
    g.setColor(new Color(255, 235, 155))
    g.fillOval(45, -13, 5, 5)
    g.fillOval(45, 8, 5, 5)
    g.setColor(Color.WHITE)
    g.fillRect(43, -3, 7, 5)
    g.setColor(GOLD)
    g.fillRect(3, 15, 18, 5)

    // Small mirrors
    g.setColor(Color.BLACK)
    g.fillRect(48, -20, 7, 3)
    g.fillRect(48, 17, 7, 3)

    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)
    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)

    // 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 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.
====================================================================
*/