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