Code Sketch
yoiii
Category: Programming
import java.awt._
import java.awt.event._
import java.awt.geom._
import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Paths}
import java.security.MessageDigest
import java.util.Base64
import javax.swing._
import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.Set
import scala.util.Random
// ============================================================
// DESI TRUCK BATTLE ROYALE - ORIGINAL SCALA/SWING GAME
// Flow: LOGIN -> GARAGE -> CITY -> AIRPORT -> AIRPLANE
// -> LAND -> BATTLE ARENA -> RESULT -> PROFILE
// Local 2-player mode included. Progress is saved locally.
// ============================================================
val WIDTH = 1280
val HEIGHT = 760
val WORLD_W = 1800.0
val WORLD_H = 1200.0
val BG = new Color(16, 20, 26)
val DARK = new Color(24, 29, 36)
val PANEL = new Color(33, 39, 48)
val PANEL2 = new Color(45, 52, 63)
val TEXT = new Color(238, 242, 246)
val MUTED = new Color(164, 174, 184)
val GOLD = new Color(242, 194, 65)
val ROAD = new Color(65, 67, 71)
val ROAD_MARK = new Color(220, 198, 105)
val GRASS = new Color(112, 155, 88)
val GRASS2 = new Color(98, 143, 80)
val WATER = new Color(65, 125, 165)
val HOUSE = new Color(179, 151, 115)
val ROOF = new Color(111, 72, 58)
val TRUCK_RED = new Color(211, 58, 45)
val TRUCK_DARK = new Color(120, 34, 28)
val ENEMY = new Color(201, 65, 78)
val FRIEND = new Color(55, 179, 101)
val PLAYER2 = new Color(72, 153, 222)
val SKY = new Color(137, 201, 235)
val CLOUD = new Color(247, 250, 252, 190)
val SAFE = new Color(67, 190, 125)
val BLUE = new Color(91, 162, 226)
// ------------------------------------------------------------
// ACCOUNT / PROFILE STATE
// ------------------------------------------------------------
case class Account(
username: String,
playerId: String,
passwordHash: String,
points: Int,
coins: Int,
matches: Int,
bestKills: Int
)
val saveFile = Paths.get(
System.getProperty("user.home"),
".desi_truck_battle_accounts.txt"
)
var accounts = ArrayBuffer[Account]()
var currentUsername = ""
var currentPlayerId = ""
var currentPoints = 0
var currentCoins = 500
var totalMatches = 0
var currentKills = 0
var bestKills = 0
def hashPassword(text: String): String = {
val md = MessageDigest.getInstance("SHA-256")
md.digest(text.getBytes(StandardCharsets.UTF_8)).map("%02x".format(_)).mkString
}
def b64(text: String): String =
Base64.getEncoder.encodeToString(text.getBytes(StandardCharsets.UTF_8))
def unb64(text: String): String =
new String(Base64.getDecoder.decode(text), StandardCharsets.UTF_8)
def ensureSaveFile(): Unit = {
try {
if (!Files.exists(saveFile)) Files.createFile(saveFile)
} catch {
case _: Throwable => ()
}
}
def loadAccounts(): Unit = {
ensureSaveFile()
try {
val lines = Files.readAllLines(saveFile, StandardCharsets.UTF_8)
val it = lines.iterator()
while (it.hasNext) {
val p = it.next().split("\\|", -1)
if (p.length >= 7) {
try {
accounts += Account(
unb64(p(0)),
unb64(p(1)),
unb64(p(2)),
p(3).toInt,
p(4).toInt,
p(5).toInt,
p(6).toInt
)
} catch {
case _: Throwable => ()
}
}
}
} catch {
case _: Throwable => ()
}
}
def saveAccounts(): Unit = {
try {
val text = accounts.map { a =>
Seq(
b64(a.username),
b64(a.playerId),
b64(a.passwordHash),
a.points.toString,
a.coins.toString,
a.matches.toString,
a.bestKills.toString
).mkString("|")
}.mkString("\n")
Files.write(saveFile, text.getBytes(StandardCharsets.UTF_8))
} catch {
case _: Throwable => ()
}
}
def uniquePlayerId(): String = {
var id = ""
var used = true
while (used) {
id = "IND-" + (100000 + rng.nextInt(900000)).toString
used = accounts.exists(_.playerId == id)
}
id
}
def saveCurrent(): Unit = {
val idx = accounts.indexWhere(_.username.equalsIgnoreCase(currentUsername))
if (idx >= 0) {
val old = accounts(idx)
accounts.update(
idx,
old.copy(
points = currentPoints,
coins = currentCoins,
matches = totalMatches,
bestKills = Math.max(bestKills, old.bestKills)
)
)
saveAccounts()
}
}
// ------------------------------------------------------------
// WORLD DATA
// ------------------------------------------------------------
case class Building(x: Double, y: Double, w: Double, h: Double, name: String, floors: Int)
case class Traffic(x: Double, y: Double, dx: Double, dy: Double, kind: String)
case class Enemy(var x: Double, var y: Double, var hp: Int, var alive: Boolean, var cool: Int, name: String)
case class Loot(x: Double, y: Double, kind: String, amount: Int, var taken: Boolean)
case class Bullet(var x: Double, var y: Double, vx: Double, vy: Double, owner: Int, var alive: Boolean)
val rng = new Random()
val buildings = ArrayBuffer[Building]()
val traffic = ArrayBuffer[Traffic]()
val enemies = ArrayBuffer[Enemy]()
val loot = ArrayBuffer[Loot]()
val bullets = ArrayBuffer[Bullet]()
val held = Set[String]()
val gunNames = Array("Pistol", "SMG", "Rifle", "Shotgun")
val gunDamage = Array(18, 12, 25, 36)
val gunAmmo = Array(60, 120, 90, 42)
val gunCost = Array(0, 300, 650, 900)
val fireGap = Array(13, 7, 10, 18)
// ------------------------------------------------------------
// GAME FLOW STATE
// ------------------------------------------------------------
var screen = "LOGIN"
var message = "Welcome to DESI TRUCK BATTLE ROYALE"
var messageLife = 240
var truckX = 190.0
var truckY = 1060.0
var truckAngle = -Math.PI / 2.0
var truckSpeed = 0.0
var truckFuel = 100.0
var planeProgress = 0.0
var planeAltitude = 1000.0
var landReady = false
var flightBoost = 0
// Animation / transition counters
// These must exist at top level because the city crash and airplane landing
// states are updated from the shared game loop.
var crashTicks = 0
var landingTicks = 0
var playerX = 900.0
var playerY = 620.0
var playerAngle = 0.0
var hp = 100
var armor = 20
var medkits = 2
var ammo = 60
var gun = 0
var lootCount = 0
var shots = 0
var hits = 0
var battleTicks = 0
var battleActive = false
var twoPlayer = false
var p2X = 770.0
var p2Y = 620.0
var p2HP = 100
var gameWon = false
var tickNo = 0L
var lastTime = System.currentTimeMillis()
// ------------------------------------------------------------
// UI / WINDOW
// ------------------------------------------------------------
val frame = new JFrame("DESI TRUCK BATTLE ROYALE")
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE)
frame.setSize(WIDTH, HEIGHT)
frame.setResizable(false)
frame.setLocationRelativeTo(null)
var panel: GamePanel = null
var loginUser = new JTextField(18)
var loginPass = new JPasswordField(18)
def showMessage(text: String): Unit = {
message = text
messageLife = 220
}
def makeButton(label: String)(action: => Unit): JButton = {
val b = new JButton(label)
b.setFont(new Font("SansSerif", Font.BOLD, 15))
b.setForeground(TEXT)
b.setBackground(PANEL2)
b.setFocusPainted(false)
b.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = action
})
b
}
// ------------------------------------------------------------
// LOGIN SCREEN
// ------------------------------------------------------------
def buildLogin(): Unit = {
val root = new JPanel(new BorderLayout(22, 22))
root.setBorder(BorderFactory.createEmptyBorder(25, 34, 25, 34))
root.setBackground(BG)
val title = new JLabel("DESI TRUCK BATTLE ROYALE", SwingConstants.CENTER)
title.setForeground(GOLD)
title.setFont(new Font("SansSerif", Font.BOLD, 36))
root.add(title, BorderLayout.NORTH)
val info = new JTextArea()
info.setEditable(false)
info.setLineWrap(true)
info.setWrapStyleWord(true)
info.setBackground(PANEL)
info.setForeground(TEXT)
info.setFont(new Font("SansSerif", Font.PLAIN, 16))
info.setBorder(BorderFactory.createEmptyBorder(22, 22, 22, 22))
info.setText(
"DESI ROAD TO BATTLE\n\n" +
"Create your own player account. Your Player ID, points, coins and match history are saved locally.\n\n" +
"MISSION FLOW\n" +
"1. Take your Indian-style cargo truck.\n" +
"2. Drive through a large city with buildings, roads and traffic.\n" +
"3. Reach the airport.\n" +
"4. Enter the aircraft.\n" +
"5. Fly until the LAND button becomes available.\n" +
"6. Land at the original battle arena.\n" +
"7. Explore houses and collect useful loot.\n" +
"8. Fight enemy bots, survive the safe zone and earn points.\n" +
"9. Results are saved to the same account.\n\n" +
"LOCAL 2-PLAYER\n" +
"P1 = WASD + SPACE\n" +
"P2 = IJKL + U\n\n" +
"This project uses original Java2D visuals and does not copy copyrighted game assets or maps."
)
val form = new JPanel()
form.setBackground(PANEL)
form.setLayout(new BoxLayout(form, BoxLayout.Y_AXIS))
form.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24))
val heading = new JLabel("PLAYER ACCOUNT")
heading.setForeground(TEXT)
heading.setFont(new Font("SansSerif", Font.BOLD, 21))
form.add(heading)
form.add(Box.createVerticalStrut(14))
val ul = new JLabel("Username")
ul.setForeground(MUTED)
form.add(ul)
loginUser = new JTextField(18)
loginUser.setFont(new Font("SansSerif", Font.PLAIN, 16))
form.add(loginUser)
form.add(Box.createVerticalStrut(12))
val pl = new JLabel("Password")
pl.setForeground(MUTED)
form.add(pl)
loginPass = new JPasswordField(18)
loginPass.setFont(new Font("SansSerif", Font.PLAIN, 16))
form.add(loginPass)
form.add(Box.createVerticalStrut(18))
val sign = makeButton("SIGN IN") {
val u = loginUser.getText.trim
val p = loginPass.getPassword.mkString
accounts.find(_.username.equalsIgnoreCase(u)) match {
case Some(a) if a.passwordHash == hashPassword(p) =>
currentUsername = a.username
currentPlayerId = a.playerId
currentPoints = a.points
currentCoins = a.coins
totalMatches = a.matches
bestKills = a.bestKills
currentKills = 0
buildCity()
screen = "GARAGE"
showGame()
showMessage("Welcome back, " + currentUsername + "!")
case _ =>
JOptionPane.showMessageDialog(frame, "Wrong username or password.", "SIGN IN", JOptionPane.WARNING_MESSAGE)
}
}
val create = makeButton("NEW ACCOUNT") {
val u = loginUser.getText.trim
val p = loginPass.getPassword.mkString
if (u.length < 3 || p.length < 4) {
JOptionPane.showMessageDialog(frame, "Use at least 3 characters for username and 4 for password.")
} else if (accounts.exists(_.username.equalsIgnoreCase(u))) {
JOptionPane.showMessageDialog(frame, "That username already exists. Use SIGN IN.")
} else {
currentUsername = u
currentPlayerId = uniquePlayerId()
currentPoints = 0
currentCoins = 500
totalMatches = 0
bestKills = 0
currentKills = 0
accounts += Account(currentUsername, currentPlayerId, hashPassword(p), 0, 500, 0, 0)
saveAccounts()
JOptionPane.showMessageDialog(
frame,
"Account created!\n\nUsername: " + currentUsername + "\nPlayer ID: " + currentPlayerId + "\nCoins: 500",
"NEW PLAYER",
JOptionPane.INFORMATION_MESSAGE
)
buildCity()
screen = "GARAGE"
showGame()
}
}
val guest = makeButton("DEMO GUEST") {
currentUsername = "Guest"
currentPlayerId = "DEMO-000001"
currentPoints = 0
currentCoins = 500
totalMatches = 0
currentKills = 0
bestKills = 0
buildCity()
screen = "GARAGE"
showGame()
showMessage("Demo guest started")
}
form.add(sign)
form.add(Box.createVerticalStrut(10))
form.add(create)
form.add(Box.createVerticalStrut(10))
form.add(guest)
val split = new JPanel(new GridLayout(1, 2, 24, 0))
split.setOpaque(false)
split.add(info)
split.add(form)
root.add(split, BorderLayout.CENTER)
val footer = new JLabel("Original game concept ? Offline/local save ? No copyrighted game files", SwingConstants.CENTER)
footer.setForeground(MUTED)
footer.setFont(new Font("SansSerif", Font.PLAIN, 12))
root.add(footer, BorderLayout.SOUTH)
frame.setContentPane(root)
frame.revalidate()
frame.repaint()
}
// ------------------------------------------------------------
// WORLD GENERATION
// ------------------------------------------------------------
def buildCity(): Unit = {
crashTicks = 0
landingTicks = 0
landReady = false
planeProgress = 0.0
buildings.clear()
traffic.clear()
val names = Array(
"Ganesh Chowk", "Market", "Workshop", "School", "Clinic",
"Warehouse", "Truck Stand", "Small Hotel", "Factory Gate", "Temple Lane",
"Bus Depot", "Old Market", "Farm House", "Service Center", "Airport Road"
)
val xs = Array(80, 365, 700, 1040, 1370, 80, 365, 700, 1040, 1370, 80, 365, 700, 1040, 1370)
val ys = Array(80, 80, 80, 80, 80, 430, 430, 430, 430, 430, 780, 780, 780, 780, 780)
var i = 0
while (i < names.length) {
buildings += Building(
xs(i).toDouble,
ys(i).toDouble,
185 + (i % 3) * 36,
155 + (i % 2) * 55,
names(i),
1 + i % 3
)
i += 1
}
// Big multi-storey landmarks
buildings += Building(520, 115, 220, 300, "CITY MALL TOWER", 8)
buildings += Building(1240, 120, 280, 320, "MAHANAGAR OFFICE", 10)
buildings += Building(520, 670, 250, 250, "GRAND HOSPITAL", 7)
buildings += Building(1220, 680, 300, 260, "MEGA WAREHOUSE", 6)
var j = 0
while (j < 30) {
traffic += Traffic(
(30 + ((j * 211) % 1740)).toDouble,
(290 + ((j * 173) % 760)).toDouble,
if (j % 2 == 0) 1.0 else -1.0,
0.0,
if (j % 5 == 0) "BUS" else if (j % 3 == 0) "AUTO" else "CAR"
)
j += 1
}
truckX = 190
truckY = 1060
truckAngle = -Math.PI / 2.0
truckSpeed = 0
truckFuel = 100
}
def buildBattle(): Unit = {
landingTicks = 0
enemies.clear()
loot.clear()
bullets.clear()
val spawns = Array(
(210, 190), (460, 190), (760, 230), (1080, 180), (1430, 230),
(260, 510), (620, 500), (1010, 470), (1390, 520),
(380, 850), (780, 880), (1160, 850), (1510, 880)
)
var i = 0
while (i < spawns.length) {
enemies += Enemy(
spawns(i)._1.toDouble,
spawns(i)._2.toDouble,
75 + rng.nextInt(45),
true,
rng.nextInt(25),
"ENEMY-" + (i + 1)
)
i += 1
}
val kinds = Array("AMMO", "MEDKIT", "ARMOR", "SMG", "RIFLE", "COINS")
var j = 0
while (j < 55) {
loot += Loot(
(90 + rng.nextInt(1620)).toDouble,
(100 + rng.nextInt(980)).toDouble,
kinds(rng.nextInt(kinds.length)),
10 + rng.nextInt(45),
false
)
j += 1
}
playerX = 900
playerY = 1020
playerAngle = -Math.PI / 2
hp = 100
armor = 20
medkits = 2
gun = 0
ammo = gunAmmo(gun)
lootCount = 0
shots = 0
hits = 0
battleTicks = 0
currentKills = 0
gameWon = false
battleActive = true
twoPlayer = false
p2X = 780
p2Y = 1030
p2HP = 100
}
// ------------------------------------------------------------
// GAME PANEL
// ------------------------------------------------------------
class GamePanel extends JPanel with KeyListener {
setFocusable(true)
addKeyListener(this)
addMouseListener(new MouseAdapter {
override def mousePressed(e: MouseEvent): Unit = {
requestFocusInWindow()
if (screen == "FLIGHT" && landReady &&
e.getX >= 875 && e.getX <= 1165 &&
e.getY >= 445 && e.getY <= 550) {
startLanding()
}
}
})
val timer = new javax.swing.Timer(30, new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = gameTick()
})
timer.start()
override def paintComponent(g: Graphics): Unit = {
super.paintComponent(g)
val gg = g.asInstanceOf[Graphics2D]
gg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
drawHeader(gg)
screen match {
case "GARAGE" => drawGarage(gg)
case "CITY" => drawCityView(gg)
case "AIRPORT" => drawAirport(gg)
case "FLIGHT" => drawFlight(gg)
case "LANDING" => drawLanding(gg)
case "BATTLE" => drawBattleView(gg)
case "RESULT" => drawResult(gg)
case "PROFILE" => drawProfile(gg)
case _ => drawGarage(gg)
}
drawFooter(gg)
}
// ----------------------------------------------------------
// HEADER / FOOTER
// ----------------------------------------------------------
def drawHeader(g: Graphics2D): Unit = {
g.setColor(new Color(10, 13, 17))
g.fillRect(0, 0, WIDTH, 68)
g.setColor(GOLD)
g.setFont(new Font("SansSerif", Font.BOLD, 22))
g.drawString("DESI TRUCK BATTLE ROYALE", 20, 28)
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.PLAIN, 12))
g.drawString("PLAYER: " + currentUsername + " ID: " + currentPlayerId, 20, 50)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 14))
g.drawString("POINTS " + currentPoints, 865, 24)
g.drawString("COINS " + currentCoins, 1005, 24)
g.drawString("MATCHES " + totalMatches, 1135, 24)
g.setColor(SAFE)
g.drawString(screen, 865, 48)
}
def drawFooter(g: Graphics2D): Unit = {
g.setColor(new Color(8, 11, 14, 235))
g.fillRect(0, HEIGHT - 34, WIDTH, 34)
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.PLAIN, 11))
g.drawString(
"ENTER continue P profile M local 2P ESC back/logout Original Java2D/Swing project",
18,
HEIGHT - 13
)
if (messageLife > 0) {
g.setColor(GOLD)
g.setFont(new Font("SansSerif", Font.BOLD, 13))
g.drawString(message, 430, HEIGHT - 13)
}
}
// ----------------------------------------------------------
// GARAGE
// ----------------------------------------------------------
def drawGarage(g: Graphics2D): Unit = {
g.setColor(BG)
g.fillRect(0, 68, WIDTH, HEIGHT - 68)
g.setColor(PANEL)
g.fillRoundRect(45, 105, 1190, 535, 20, 20)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 29))
g.drawString("INDIAN TRUCK GARAGE", 80, 150)
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.PLAIN, 15))
g.drawString("Start from the road. Your destination is the airport, then the sky, then the battle arena.", 80, 178)
drawBigTruck(g, 180, 300)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 18))
g.drawString("MISSION", 690, 245)
val steps = Array(
"1 ENTER CITY ROAD",
"2 DRIVE TO AIRPORT",
"3 ENTER AIRPLANE",
"4 FLY UNTIL LAND IS READY",
"5 PRESS L TO LAND",
"6 LOOT HOUSES",
"7 SURVIVE ENEMIES",
"8 SCORE + SAVE"
)
g.setFont(new Font("SansSerif", Font.PLAIN, 16))
var yy = 280
var i = 0
while (i < steps.length) {
g.setColor(if (i < 3) SAFE else TEXT)
g.drawString(steps(i), 690, yy)
yy += 37
i += 1
}
g.setColor(GOLD)
g.setFont(new Font("SansSerif", Font.BOLD, 20))
g.drawString("PRESS ENTER TO START", 390, 595)
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.PLAIN, 13))
g.drawString("M toggles local two-player mode", 390, 620)
}
def drawBigTruck(g: Graphics2D, x: Int, y: Int): Unit = {
g.setColor(new Color(12, 15, 18))
g.fillOval(x - 130, y + 130, 390, 38)
g.setColor(TRUCK_DARK)
g.fillRoundRect(x, y, 350, 165, 30, 30)
g.setColor(TRUCK_RED)
g.fillRoundRect(x + 15, y + 16, 220, 133, 22, 22)
g.setColor(new Color(86, 147, 198))
g.fillRoundRect(x + 246, y + 30, 80, 85, 12, 12)
g.setColor(new Color(200, 230, 245))
g.fillRect(x + 259, y + 42, 52, 19)
g.fillRect(x + 259, y + 70, 52, 19)
g.setColor(new Color(235, 184, 63))
g.fillRect(x + 33, y + 62, 168, 38)
g.setColor(new Color(95, 42, 31))
g.setFont(new Font("SansSerif", Font.BOLD, 16))
g.drawString("BHARAT CARGO", x + 54, y + 87)
g.setColor(Color.BLACK)
g.fillOval(x + 35, y + 130, 64, 64)
g.fillOval(x + 258, y + 130, 64, 64)
g.setColor(new Color(148, 148, 148))
g.fillOval(x + 51, y + 146, 32, 32)
g.fillOval(x + 274, y + 146, 32, 32)
g.setColor(Color.WHITE)
g.setFont(new Font("SansSerif", Font.BOLD, 12))
g.drawString("MH ? ROAD KING", x + 119, y + 132)
}
// ----------------------------------------------------------
// CITY
// ----------------------------------------------------------
def drawCityView(g: Graphics2D): Unit = {
// ==========================================================
// FULL 3D-STYLE THIRD-PERSON ROAD VIEW
// Java2D pseudo-3D: perspective road, depth-scaled buildings,
// layered skyline, traffic and a large detailed Indian truck.
// ==========================================================
drawSkyAndHorizon(g)
val horizon = 245
// Distant city silhouettes
var city = 0
while (city < 18) {
val bx = 18 + city * 76
val bh = 35 + (city * 17) % 85
val bw = 52 + (city * 11) % 45
g.setColor(
if (city % 3 == 0) new Color(82, 94, 107)
else if (city % 3 == 1) new Color(94, 103, 111)
else new Color(74, 88, 98)
)
g.fillRect(bx, horizon - bh, bw, bh)
g.setColor(new Color(145, 174, 190))
var wy = horizon - bh + 10
while (wy < horizon - 8) {
var wx = bx + 8
while (wx < bx + bw - 7) {
g.fillRect(wx, wy, 5, 7)
wx += 15
}
wy += 16
}
city += 1
}
// Ground
g.setColor(new Color(105, 146, 79))
g.fillRect(0, horizon, WIDTH, HEIGHT - horizon)
// Far horizon haze
g.setColor(new Color(218, 224, 204, 120))
g.fillRect(0, horizon - 22, WIDTH, 30)
// Main perspective road
val vanX = WIDTH / 2
val vanY = horizon + 12
val roadBottomLeft = 70
val roadBottomRight = WIDTH - 70
val roadTopLeft = vanX - 55
val roadTopRight = vanX + 55
val roadPoly = new Polygon()
roadPoly.addPoint(roadTopLeft, vanY)
roadPoly.addPoint(roadTopRight, vanY)
roadPoly.addPoint(roadBottomRight, HEIGHT)
roadPoly.addPoint(roadBottomLeft, HEIGHT)
g.setColor(new Color(58, 60, 64))
g.fillPolygon(roadPoly)
// Road shoulders
g.setColor(new Color(190, 169, 113))
val leftShoulder = new Polygon()
leftShoulder.addPoint(roadTopLeft - 8, vanY)
leftShoulder.addPoint(roadTopLeft, vanY)
leftShoulder.addPoint(roadBottomLeft, HEIGHT)
leftShoulder.addPoint(roadBottomLeft - 38, HEIGHT)
g.fillPolygon(leftShoulder)
val rightShoulder = new Polygon()
rightShoulder.addPoint(roadTopRight, vanY)
rightShoulder.addPoint(roadTopRight + 8, vanY)
rightShoulder.addPoint(roadBottomRight + 38, HEIGHT)
rightShoulder.addPoint(roadBottomRight, HEIGHT)
g.fillPolygon(rightShoulder)
// Perspective lane markings
g.setColor(ROAD_MARK)
var d = 0
while (d < 12) {
val t = d / 12.0
val y1 = (vanY + t * t * (HEIGHT - vanY)).toInt
val t2 = (d + 0.52) / 12.0
val y2 = (vanY + Math.min(1.0, t2) * Math.min(1.0, t2) * (HEIGHT - vanY)).toInt
val half1 = (4 + t * 15).toInt
val half2 = (4 + Math.min(1.0, t2) * 15).toInt
val lanePoly = new Polygon()
lanePoly.addPoint(vanX - half1, y1)
lanePoly.addPoint(vanX + half1, y1)
lanePoly.addPoint(vanX + half2, y2)
lanePoly.addPoint(vanX - half2, y2)
g.fillPolygon(lanePoly)
d += 1
}
// Side poles / trees for depth
var sideObj = 0
while (sideObj < 20) {
val z = 0.15 + sideObj / 20.0
val sy = (vanY + z * z * (HEIGHT - vanY)).toInt
val sxL = (vanX - (70 + z * 540)).toInt
val sxR = (vanX + (70 + z * 540)).toInt
val hh = (18 + z * 105).toInt
drawRoadsideTree(g, sxL, sy, hh)
drawRoadsideTree(g, sxR, sy, hh)
sideObj += 1
}
// Project nearby buildings into perspective from world coordinates
var b = 0
while (b < buildings.length) {
val q = buildings(b)
drawPerspectiveBuilding(g, q)
b += 1
}
// Traffic projected from world coordinates
var tr = 0
while (tr < traffic.length) {
val q = traffic(tr)
drawPerspectiveTraffic(g, q)
tr += 1
}
// Airport billboard / waypoint
val ap = projectRelative(1080.0, 970.0)
if (ap._3 > 0) {
val ax = ap._1
val ay = ap._2
val ah = Math.max(35, Math.min(150, (5600.0 / ap._3).toInt))
g.setColor(new Color(245, 248, 250, 235))
g.fillRoundRect(ax - 70, ay - ah, 140, 48, 12, 12)
g.setColor(GOLD)
g.fillOval(ax - 14, ay - ah - 28, 28, 28)
g.setColor(Color.BLACK)
g.setFont(new Font("SansSerif", Font.BOLD, 12))
g.drawString("A", ax - 5, ay - ah - 8)
g.setColor(new Color(48, 56, 65))
g.drawString("DESI AIRPORT", ax - 45, ay - ah + 28)
g.setColor(SAFE)
g.setFont(new Font("SansSerif", Font.BOLD, 13))
g.drawString("DIST " + ap._3.toInt + "m", ax - 43, ay + 22)
}
// Detailed player truck with ground shadow, lights and cab driver
draw3DTruck(g, WIDTH / 2, HEIGHT - 135, truckAngle, Math.max(0.88, Math.min(1.22, 0.92 + Math.abs(truckSpeed) * 0.035)))
// Cockpit-style HUD
drawBar(g, 22, 92, 260, 18, truckFuel, 100, "FUEL")
drawBar(g, 22, 121, 260, 18, Math.abs(truckSpeed) * 14, 100, "SPEED")
drawInfoBox(g, 960, 88, 290, 180, "3D ROAD HUD", Array(
"W / UP = accelerate",
"S / DOWN = reverse",
"A / D = steer",
"R = refuel / restart route",
"E = enter airport",
"M = local 2P",
"Camera: THIRD PERSON 3D"
))
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 14))
g.drawString("BHARAT NAGAR HIGHWAY", 24, 176)
g.setFont(new Font("SansSerif", Font.PLAIN, 12))
g.setColor(MUTED)
g.drawString("Perspective depth ? buildings ? traffic ? road markers ? Indian roadside details", 24, 198)
drawMiniMap(g)
if (crashTicks > 0) {
g.setColor(new Color(10, 12, 16, 225))
g.fillRect(0, 68, WIDTH, HEIGHT - 68)
g.setColor(ENEMY)
g.setFont(new Font("SansSerif", Font.BOLD, 43))
g.drawString("TRUCK ACCIDENT", 454, 305)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.PLAIN, 20))
g.drawString("Collision detected ? automatic route restart", 410, 346)
}
}
def drawSkyAndHorizon(g: Graphics2D): Unit = {
var y = 68
while (y < 250) {
val q = (y - 68).toDouble / 182.0
val r = (75 * (1 - q) + 185 * q).toInt
val gg = (145 * (1 - q) + 218 * q).toInt
val b = (210 * (1 - q) + 238 * q).toInt
g.setColor(new Color(r, gg, b))
g.fillRect(0, y, WIDTH, 1)
y += 1
}
g.setColor(new Color(252, 249, 211))
g.fillOval(1070, 98, 75, 75)
// soft clouds
var c = 0
while (c < 8) {
val cx = 40 + c * 165
val cy = 105 + (c % 3) * 38
g.setColor(new Color(255, 255, 255, 125))
g.fillOval(cx, cy, 82, 28)
g.fillOval(cx + 25, cy - 15, 78, 48)
c += 1
}
}
def projectRelative(wx: Double, wy: Double): (Int, Int, Double) = {
val dx = wx - truckX
val dy = wy - truckY
val fwdX = Math.cos(truckAngle)
val fwdY = Math.sin(truckAngle)
val rightX = -fwdY
val rightY = fwdX
val depth = dx * fwdX + dy * fwdY
val side = dx * rightX + dy * rightY
if (depth <= 35) (0, 0, depth)
else {
val focal = 430.0
val sx = WIDTH / 2.0 + side * focal / depth
val sy = 250.0 + 2550.0 / depth
(sx.toInt, sy.toInt, depth)
}
}
def drawPerspectiveBuilding(g: Graphics2D, b: Building): Unit = {
val p = projectRelative(b.x + b.w / 2, b.y + b.h)
if (p._3 > 40 && p._3 < 1250 && p._1 > -180 && p._1 < WIDTH + 180) {
val scale = Math.max(0.08, Math.min(1.7, 360.0 / p._3))
val ww = Math.max(18, (b.w * 0.42 * scale).toInt)
val hh = Math.max(25, (b.h * 1.0 * scale * (0.65 + b.floors * 0.06)).toInt)
val baseY = p._2
val x = p._1 - ww / 2
val y = baseY - hh
val depth = Math.max(6, (ww * 0.16).toInt)
g.setColor(new Color(88, 75, 65))
val side = new Polygon()
side.addPoint(x + ww, y + 8)
side.addPoint(x + ww + depth, y)
side.addPoint(x + ww + depth, baseY - 5)
side.addPoint(x + ww, baseY)
g.fillPolygon(side)
g.setColor(if (b.floors > 5) new Color(137, 124, 111) else new Color(173, 143, 106))
g.fillRoundRect(x, y, ww, hh, 8, 8)
val roof = new Polygon()
roof.addPoint(x - 4, y + 8)
roof.addPoint(x + ww / 2, y - Math.max(8, (12 * scale).toInt))
roof.addPoint(x + ww + 4, y + 8)
g.setColor(if (b.floors > 5) new Color(64, 66, 73) else new Color(106, 69, 54))
g.fillPolygon(roof)
val floorsVisible = Math.max(2, Math.min(7, b.floors))
var row = 0
while (row < floorsVisible && ww > 30) {
var wx = x + 6
val wy = y + 10 + row * Math.max(9, hh / floorsVisible)
while (wx < x + ww - 8) {
g.setColor(if ((row + wx) % 2 == 0) new Color(82, 153, 194) else new Color(63, 128, 168))
g.fillRect(wx, wy, Math.max(3, (7 * scale).toInt), Math.max(4, (7 * scale).toInt))
wx += Math.max(12, (18 * scale).toInt)
}
row += 1
}
if (scale > 0.25) {
g.setColor(new Color(242, 236, 218))
g.setFont(new Font("SansSerif", Font.BOLD, 9))
g.drawString(b.name, x + 4, y + 12)
}
}
}
def drawPerspectiveTraffic(g: Graphics2D, q: Traffic): Unit = {
val p = projectRelative(q.x, q.y)
if (p._3 > 30 && p._3 < 1000 && p._1 > -80 && p._1 < WIDTH + 80) {
val scale = Math.max(0.12, Math.min(1.25, 280.0 / p._3))
val w = Math.max(9, (42 * scale).toInt)
val h = Math.max(6, (22 * scale).toInt)
val x = p._1 - w / 2
val y = p._2 - h
g.setColor(new Color(12, 14, 18, 110))
g.fillOval(x - 3, y + h - 1, w + 8, Math.max(4, h / 3))
g.setColor(
if (q.kind == "BUS") new Color(238, 178, 53)
else if (q.kind == "AUTO") new Color(58, 150, 72)
else new Color(70, 95, 161)
)
g.fillRoundRect(x, y, w, h, 5, 5)
g.setColor(new Color(195, 225, 238))
g.fillRect(x + Math.max(2, w / 7), y + 2, Math.max(3, w - w / 4), Math.max(2, h / 3))
}
}
def drawRoadsideTree(g: Graphics2D, x: Int, y: Int, h: Int): Unit = {
val trunkW = Math.max(3, h / 9)
g.setColor(new Color(100, 69, 45))
g.fillRect(x - trunkW / 2, y - h / 3, trunkW, h / 3)
g.setColor(new Color(47, 122, 64, 235))
g.fillOval(x - h / 4, y - h, h / 2, h / 2)
g.fillOval(x - h / 7, y - h - h / 5, h / 2, h / 2)
g.fillOval(x - h / 2, y - h / 2, h / 2, h / 2)
}
def worldToScreenX(x: Double): Int = ((x / WORLD_W) * WIDTH).toInt
def cityWorldY(y: Double): Int = 68 + ((y / WORLD_H) * (HEIGHT - 68)).toInt
def drawBuilding(g: Graphics2D, b: Building): Unit = {
val x = worldToScreenX(b.x)
val y = cityWorldY(b.y)
val w = Math.max(72, worldToScreenX(b.x + b.w) - x)
val h = Math.max(58, cityWorldY(b.y + b.h) - y)
val depth = Math.max(10, w / 12)
// pseudo-3D side
g.setColor(new Color(105, 91, 76))
val side = new Polygon()
side.addPoint(x + w, y + 8)
side.addPoint(x + w + depth, y)
side.addPoint(x + w + depth, y + h - 8)
side.addPoint(x + w, y + h)
g.fillPolygon(side)
g.setColor(if (b.floors % 2 == 0) HOUSE else new Color(160, 141, 113))
g.fillRoundRect(x, y, w, h, 9, 9)
val roof = new Polygon()
roof.addPoint(x - 4, y + 8)
roof.addPoint(x + w / 2, y - 20)
roof.addPoint(x + w + 4, y + 8)
g.setColor(if (b.floors > 5) new Color(70, 68, 76) else ROOF)
g.fillPolygon(roof)
// windows on every level
val levels = Math.max(2, Math.min(7, b.floors))
var row = 0
while (row < levels) {
var xx = x + 12
val yy = y + 23 + row * Math.max(18, (h - 35) / levels)
while (xx < x + w - 15) {
g.setColor(if ((row + xx) % 2 == 0) new Color(83, 149, 188) else new Color(67, 124, 165))
g.fillRect(xx, yy, 9, 10)
xx += 23
}
row += 1
}
// doors / shopfronts
g.setColor(new Color(72, 54, 42))
g.fillRect(x + w / 2 - 10, y + h - 28, 20, 28)
g.setColor(Color.WHITE)
g.setFont(new Font("SansSerif", Font.BOLD, 8))
g.drawString(b.name, x + 5, y + h - 6)
}
def drawTraffic(g: Graphics2D, x: Int, y: Int, kind: String): Unit = {
val w = if (kind == "BUS") 54 else if (kind == "AUTO") 34 else 42
val h = if (kind == "BUS") 25 else 20
g.setColor(
kind match {
case "BUS" => new Color(238, 178, 53)
case "AUTO" => new Color(54, 146, 73)
case _ => new Color(73, 95, 160)
}
)
g.fillRoundRect(x, y, w, h, 7, 7)
g.setColor(new Color(195, 225, 238))
g.fillRect(x + 8, y + 4, Math.max(10, w - 16), 7)
g.setColor(Color.BLACK)
g.fillOval(x + 5, y + h - 5, 8, 8)
g.fillOval(x + w - 13, y + h - 5, 8, 8)
}
def drawTruck(g: Graphics2D, x: Int, y: Int, angle: Double): Unit = {
draw3DTruck(g, x, y, angle, 1.0)
}
def draw3DTruck(g: Graphics2D, x: Int, y: Int, angle: Double, scale: Double): Unit = {
val old = g.getTransform
g.translate(x, y)
g.rotate(angle)
g.scale(scale, scale)
// Ground shadow
g.setColor(new Color(5, 7, 9, 150))
g.fillOval(-155, 46, 310, 58)
g.setColor(new Color(30, 32, 35, 120))
g.fillOval(-115, 60, 230, 24)
// Rear cargo box: depth layers
g.setColor(new Color(100, 27, 23))
g.fillRoundRect(-120, -104, 144, 112, 18, 18)
g.setColor(new Color(170, 45, 37))
g.fillRoundRect(-113, -100, 135, 104, 16, 16)
g.setColor(new Color(224, 71, 53))
g.fillRoundRect(-103, -94, 117, 92, 14, 14)
// Cargo ribs
g.setColor(new Color(128, 35, 30))
var rib = -90
while (rib <= 0) {
g.fillRect(rib, -89, 6, 82)
rib += 23
}
// Decorative Indian stripes
g.setColor(new Color(242, 191, 55))
g.fillRect(-102, -55, 112, 12)
g.setColor(new Color(77, 145, 82))
g.fillRect(-102, -42, 112, 7)
// Rear cargo sign
g.setColor(new Color(244, 224, 174))
g.fillRoundRect(-82, -31, 83, 25, 8, 8)
g.setColor(new Color(74, 43, 34))
g.setFont(new Font("SansSerif", Font.BOLD, 13))
g.drawString("BHARAT", -66, -14)
// Chassis
g.setColor(new Color(57, 55, 52))
g.fillRoundRect(-135, -5, 210, 32, 12, 12)
g.setColor(new Color(32, 34, 36))
g.fillRect(-135, 13, 210, 9)
// Cab with 3D front and side
g.setColor(new Color(111, 31, 27))
g.fillRoundRect(12, -75, 105, 101, 24, 24)
g.setColor(new Color(195, 52, 42))
g.fillRoundRect(19, -70, 92, 93, 20, 20)
// windshield
g.setColor(new Color(54, 119, 157))
g.fillRoundRect(38, -61, 58, 45, 11, 11)
g.setColor(new Color(188, 225, 240))
g.fillRoundRect(44, -55, 46, 31, 7, 7)
g.setColor(new Color(104, 160, 189, 140))
g.fillRect(66, -56, 5, 32)
// Hood / front face
g.setColor(new Color(151, 37, 31))
g.fillRoundRect(74, -11, 50, 37, 10, 10)
g.setColor(new Color(76, 82, 87))
g.fillRect(84, -2, 30, 12)
// Driver inside cab
g.setColor(new Color(35, 35, 37))
g.fillOval(56, -43, 25, 25)
g.setColor(new Color(199, 143, 102))
g.fillOval(61, -45, 15, 15)
g.setColor(new Color(40, 72, 102))
g.fillRoundRect(57, -31, 24, 27, 7, 7)
g.setColor(new Color(22, 25, 29))
g.fillOval(52, -42, 31, 8)
// Mirrors
g.setColor(new Color(32, 35, 38))
g.fillRoundRect(104, -65, 22, 8, 4, 4)
g.fillRoundRect(104, 20, 22, 8, 4, 4)
g.setColor(new Color(100, 150, 173))
g.fillRect(111, -63, 10, 5)
g.fillRect(111, 22, 10, 5)
// Wheels and hubs
val wheelX = Array(-70, 54)
var wi = 0
while (wi < wheelX.length) {
g.setColor(Color.BLACK)
g.fillOval(wheelX(wi), -10, 46, 46)
g.setColor(new Color(52, 54, 57))
g.fillOval(wheelX(wi) + 6, -4, 34, 34)
g.setColor(new Color(178, 181, 184))
g.fillOval(wheelX(wi) + 15, 5, 16, 16)
g.setColor(new Color(111, 114, 118))
g.fillOval(wheelX(wi) + 19, 9, 8, 8)
wi += 1
}
// Headlights
g.setColor(new Color(255, 238, 160))
g.fillOval(110, -3, 11, 11)
g.fillOval(110, 13, 11, 11)
g.setColor(new Color(255, 251, 210, 120))
g.fillOval(116, -7, 23, 19)
g.fillOval(116, 9, 23, 19)
// Front grill + plate
g.setColor(new Color(35, 39, 42))
g.fillRoundRect(91, 3, 31, 18, 4, 4)
g.setColor(new Color(190, 195, 198))
var grillY = 7
while (grillY < 18) {
g.fillRect(95, grillY, 23, 2)
grillY += 5
}
g.setColor(Color.WHITE)
g.fillRoundRect(89, 24, 33, 9, 3, 3)
g.setColor(Color.BLACK)
g.setFont(new Font("SansSerif", Font.BOLD, 7))
g.drawString("MH 12", 93, 31)
// Bumper + side steps
g.setColor(new Color(78, 81, 84))
g.fillRoundRect(104, 28, 26, 9, 4, 4)
g.fillRoundRect(0, 22, 24, 8, 4, 4)
// Decorative name
g.setColor(new Color(245, 192, 55))
g.setFont(new Font("SansSerif", Font.BOLD, 11))
g.drawString("ROAD KING", -77, -71)
g.setTransform(old)
}
def drawMiniMap(g: Graphics2D): Unit = {
val x = 960
val y = 500
val w = 260
val h = 155
g.setColor(new Color(8, 11, 14, 225))
g.fillRoundRect(x, y, w, h, 12, 12)
g.setColor(new Color(92, 92, 98))
var i = 0
while (i < 4) {
g.fillRect(x + 30 + i * 55, y + 10, 7, h - 20)
i += 1
}
var j = 0
while (j < 3) {
g.fillRect(x + 10, y + 34 + j * 38, w - 20, 7)
j += 1
}
g.setColor(TRUCK_RED)
g.fillOval(x + (truckX * w / WORLD_W).toInt - 4, y + (truckY * h / WORLD_H).toInt - 4, 9, 9)
g.setColor(GOLD)
g.fillOval(x + (1080 * w / WORLD_W).toInt - 5, y + (970 * h / WORLD_H).toInt - 5, 11, 11)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 11))
g.drawString("CITY MINI MAP", x + 12, y + h - 9)
}
// ----------------------------------------------------------
// AIRPORT
// ----------------------------------------------------------
def drawAirport(g: Graphics2D): Unit = {
g.setColor(new Color(100, 145, 159))
g.fillRect(0, 68, WIDTH, HEIGHT - 68)
g.setColor(new Color(77, 122, 78))
g.fillRect(0, 68, WIDTH, 120)
g.fillRect(0, 585, WIDTH, 175)
g.setColor(new Color(65, 67, 72))
g.fillRect(45, 220, 1180, 350)
g.setColor(Color.WHITE)
var x = 70
while (x < 1200) {
g.fillRect(x, 382, 92, 8)
x += 140
}
// Terminal
g.setColor(new Color(211, 218, 224))
g.fillRoundRect(80, 105, 420, 96, 16, 16)
g.setColor(new Color(74, 153, 196))
g.fillRect(100, 132, 380, 20)
g.setColor(new Color(48, 56, 65))
g.setFont(new Font("SansSerif", Font.BOLD, 21))
g.drawString("DESI INTERNATIONAL AIRPORT", 108, 181)
// Hangar
g.setColor(new Color(137, 146, 157))
g.fillRoundRect(865, 105, 290, 112, 14, 14)
g.setColor(GOLD)
g.fillRect(890, 135, 240, 18)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 20))
g.drawString("FLIGHT HANGAR", 925, 190)
// Plane
drawPlane(g, 625, 392, 0)
// Airport service trucks
var i = 0
while (i < 7) {
g.setColor(if (i % 2 == 0) new Color(232, 181, 48) else new Color(76, 148, 184))
g.fillRoundRect(140 + i * 145, 603, 72, 35, 7, 7)
g.setColor(Color.BLACK)
g.fillOval(150 + i * 145, 630, 8, 8)
g.fillOval(195 + i * 145, 630, 8, 8)
i += 1
}
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 21))
g.drawString("AIRPORT CONTROL", 55, 655)
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.PLAIN, 15))
g.drawString("E = enter aircraft", 55, 683)
g.drawString("Your truck is parked outside TERMINAL 1", 55, 708)
g.drawString("B = return to city", 55, 709)
g.setColor(GOLD)
g.setFont(new Font("SansSerif", Font.BOLD, 18))
g.drawString("FLIGHT: BHARAT DROP ZONE", 820, 655)
g.setColor(SAFE)
g.drawString("Aircraft ready", 820, 683)
g.setColor(GOLD)
g.setFont(new Font("SansSerif", Font.BOLD, 15))
g.drawString("STEP 1: DRIVE HERE -> STEP 2: PRESS E", 760, 710)
}
def drawPlane(g: Graphics2D, x: Int, y: Int, angle: Double): Unit = {
val old = g.getTransform
g.translate(x, y)
g.rotate(angle)
g.setColor(new Color(225, 230, 236))
g.fillRoundRect(-105, -16, 210, 32, 18, 18)
val wingA = new Polygon()
wingA.addPoint(-15, -8)
wingA.addPoint(-68, -74)
wingA.addPoint(-1, -13)
g.setColor(new Color(115, 131, 149))
g.fillPolygon(wingA)
val wingB = new Polygon()
wingB.addPoint(-15, 8)
wingB.addPoint(-68, 74)
wingB.addPoint(-1, 13)
g.fillPolygon(wingB)
g.setColor(SKY)
g.fillRoundRect(40, -10, 36, 20, 8, 8)
g.setColor(GOLD)
g.fillRect(-80, -4, 30, 8)
// engine nacelles / 3D underside details
g.setColor(new Color(91, 101, 113))
g.fillOval(-25, -31, 28, 15)
g.fillOval(-25, 16, 28, 15)
g.setColor(new Color(42, 49, 57))
g.fillOval(-18, -28, 15, 10)
g.fillOval(-18, 18, 15, 10)
g.setColor(new Color(248, 249, 250))
g.fillRect(-91, -8, 22, 16)
g.setColor(new Color(178, 184, 191))
val tailPoly = new Polygon()
tailPoly.addPoint(78, -5)
tailPoly.addPoint(110, 0)
tailPoly.addPoint(78, 5)
g.fillPolygon(tailPoly)
g.setTransform(old)
}
// ----------------------------------------------------------
// FLIGHT
// ----------------------------------------------------------
def drawFlight(g: Graphics2D): Unit = {
var y = 68
while (y < HEIGHT) {
val q = (y - 68).toDouble / (HEIGHT - 68).toDouble
val r = (SKY.getRed * (1 - q) + 230 * q).toInt
val gg = (SKY.getGreen * (1 - q) + 245 * q).toInt
val b = (SKY.getBlue * (1 - q) + 250 * q).toInt
g.setColor(new Color(r, gg, b))
g.fillRect(0, y, WIDTH, 1)
y += 1
}
var c = 0
while (c < 13) {
val cx = 30 + c * 100
val cy = 150 + (c % 5) * 95
g.setColor(CLOUD)
g.fillOval(cx, cy, 80, 35)
g.fillOval(cx + 25, cy - 18, 80, 55)
g.fillOval(cx + 72, cy + 4, 68, 32)
c += 1
}
g.setColor(new Color(86, 139, 77))
val ground = new Polygon()
ground.addPoint(0, 640)
ground.addPoint(270, 615)
ground.addPoint(620, 665)
ground.addPoint(950, 615)
ground.addPoint(WIDTH, 645)
ground.addPoint(WIDTH, HEIGHT)
ground.addPoint(0, HEIGHT)
g.fillPolygon(ground)
g.setColor(new Color(83, 83, 88))
g.drawLine(0, 706, WIDTH, 626)
g.drawLine(240, HEIGHT, 500, 645)
g.drawLine(920, HEIGHT, 825, 635)
drawPlane(g, 630, 410, 0)
drawBar(g, 55, 105, 430, 24, planeProgress, 100, "FLIGHT PROGRESS")
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 24))
g.drawString(if (landReady) "LANDING IS READY" else "FLYING TO DROP ZONE", 55, 165)
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.PLAIN, 14))
g.drawString("W/S = altitude A/D = course SPACE = boost", 55, 194)
g.drawString("Altitude: " + planeAltitude.toInt + " m", 55, 228)
g.drawString("Drop Zone: BHARAT WILDERNESS", 55, 255)
drawInfoBox(g, 900, 220, 300, 200, "AIRCRAFT SYSTEMS", Array(
"Engine status: STABLE",
"Fuel: 86%",
"Weather: CLEAR",
"Route: BHARAT EAST",
"Landing gear: " + (if (landReady) "READY" else "LOCKED"),
"Player: " + currentPlayerId
))
if (landReady) {
g.setColor(new Color(12, 28, 22, 235))
g.fillRoundRect(875, 445, 290, 105, 18, 18)
g.setColor(SAFE)
g.setStroke(new BasicStroke(3f))
g.drawRoundRect(875, 445, 290, 105, 18, 18)
g.setFont(new Font("SansSerif", Font.BOLD, 28))
g.drawString("LAND NOW", 955, 480)
g.setFont(new Font("SansSerif", Font.BOLD, 16))
g.drawString("L or ENTER", 968, 512)
g.setFont(new Font("SansSerif", Font.PLAIN, 12))
g.drawString("Aircraft ready for descent", 944, 535)
}
}
// ----------------------------------------------------------
// BATTLE VIEW
// ----------------------------------------------------------
def drawBattleView(g: Graphics2D): Unit = {
g.setColor(GRASS)
g.fillRect(0, 68, WIDTH, HEIGHT - 68)
// ==========================================================
// 3D-STYLE BATTLE TERRAIN LAYER
// ==========================================================
drawBattlePerspectiveGround(g)
// Decorative terrain
var p = 0
while (p < 24) {
val xx = (p * 157) % WIDTH
val yy = 95 + ((p * 113) % 555)
g.setColor(if (p % 2 == 0) GRASS2 else new Color(119, 164, 92))
g.fillOval(xx, yy, 115, 68)
p += 1
}
// Roads
g.setColor(new Color(84, 84, 88))
g.fillRect(0, 325, WIDTH, 42)
g.fillRect(0, 560, WIDTH, 42)
g.fillRect(375, 68, 44, HEIGHT)
g.fillRect(830, 68, 44, HEIGHT)
g.fillRect(1115, 68, 44, HEIGHT)
g.setColor(GOLD)
var rx = 0
while (rx < WIDTH) {
g.fillRect(rx, 344, 28, 4)
g.fillRect(rx, 579, 28, 4)
rx += 60
}
// Water pond
g.setColor(WATER)
g.fillOval(1180, 395, 290, 155)
// Houses
var h = 0
while (h < 18) {
val xx = 65 + ((h * 194) % 1030)
val yy = 105 + ((h * 151) % 510)
drawBattleHouse(g, xx, yy, 110 + (h % 3) * 15, 76 + (h % 2) * 15)
h += 1
}
// Trees
var t = 0
while (t < 30) {
val xx = 25 + ((t * 263) % 1200)
val yy = 90 + ((t * 173) % 570)
drawTree(g, xx, yy)
t += 1
}
// Safe zone
val zonePulse = (tickNo % 80).toInt
val zoneR = 250 + zonePulse / 7
g.setColor(new Color(75, 160, 235, 42))
g.fillOval(640 - zoneR, 400 - zoneR, zoneR * 2, zoneR * 2)
g.setColor(new Color(100, 180, 255, 170))
g.setStroke(new BasicStroke(3f))
g.drawOval(640 - zoneR, 400 - zoneR, zoneR * 2, zoneR * 2)
// Loot
var l = 0
while (l < loot.length) {
val q = loot(l)
if (!q.taken) {
val x = worldToBattleX(q.x)
val y = worldToBattleY(q.y)
g.setColor(q.kind match {
case "AMMO" => GOLD
case "MEDKIT" => SAFE
case "ARMOR" => BLUE
case "SMG" => new Color(191, 82, 192)
case "RIFLE" => new Color(205, 100, 71)
case "COINS" => new Color(239, 183, 46)
case _ => TEXT
})
g.fillRoundRect(x - 7, y - 7, 14, 14, 4, 4)
}
l += 1
}
// Enemies
var e = 0
while (e < enemies.length) {
val q = enemies(e)
if (q.alive) {
val x = worldToBattleX(q.x)
val y = worldToBattleY(q.y)
g.setColor(ENEMY)
g.fillOval(x - 13, y - 13, 26, 26)
g.setColor(Color.WHITE)
g.fillOval(x - 6, y - 6, 4, 4)
g.fillOval(x + 2, y - 6, 4, 4)
g.setColor(Color.BLACK)
g.fillRect(x - 17, y - 26, 34, 5)
g.setColor(SAFE)
g.fillRect(x - 17, y - 26, Math.max(0, q.hp) * 34 / 120, 5)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.PLAIN, 8))
g.drawString(q.name, x - 18, y + 24)
}
e += 1
}
// Bullets
var bi = 0
while (bi < bullets.length) {
val q = bullets(bi)
if (q.alive) {
g.setColor(if (q.owner == 1) GOLD else ENEMY)
g.fillOval(worldToBattleX(q.x) - 3, worldToBattleY(q.y) - 3, 6, 6)
}
bi += 1
}
// Player 1
val px = worldToBattleX(playerX)
val py = worldToBattleY(playerY)
g.setColor(FRIEND)
g.fillOval(px - 15, py - 15, 30, 30)
g.setColor(Color.WHITE)
g.fillOval(px - 6, py - 7, 5, 5)
g.fillOval(px + 2, py - 7, 5, 5)
g.setColor(Color.BLACK)
g.fillRect(px - 11, py + 7, 22, 3)
// Player 2
if (twoPlayer) {
val p2sx = worldToBattleX(p2X)
val p2sy = worldToBattleY(p2Y)
g.setColor(PLAYER2)
g.fillOval(p2sx - 14, p2sy - 14, 28, 28)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 10))
g.drawString("P2", p2sx - 8, p2sy - 20)
}
drawBattleHUD(g)
if (hp <= 0) {
g.setColor(new Color(7, 10, 13, 205))
g.fillRect(0, 68, WIDTH, HEIGHT - 68)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 42))
g.drawString("MATCH OVER", 470, 305)
g.setFont(new Font("SansSerif", Font.PLAIN, 20))
g.drawString("Press ENTER for results", 485, 350)
}
}
def worldToBattleX(x: Double): Int = ((x / WORLD_W) * WIDTH).toInt
def worldToBattleY(y: Double): Int = 68 + ((y / WORLD_H) * (HEIGHT - 68)).toInt
def drawBattlePerspectiveGround(g: Graphics2D): Unit = {
val horizon = 195
var y = horizon
while (y < HEIGHT) {
val q = (y - horizon).toDouble / (HEIGHT - horizon).toDouble
val shade = (98 + q * 38).toInt
g.setColor(new Color(Math.min(160, shade), Math.min(175, shade + 22), Math.min(120, shade - 4)))
g.fillRect(0, y, WIDTH, 1)
y += 1
}
val vanX = WIDTH / 2
g.setColor(new Color(71, 73, 76))
val main = new Polygon()
main.addPoint(vanX - 36, horizon + 20)
main.addPoint(vanX + 36, horizon + 20)
main.addPoint(WIDTH - 80, HEIGHT)
main.addPoint(80, HEIGHT)
g.fillPolygon(main)
g.setColor(new Color(193, 165, 104))
val shL = new Polygon()
shL.addPoint(vanX - 42, horizon + 20)
shL.addPoint(vanX - 36, horizon + 20)
shL.addPoint(80, HEIGHT)
shL.addPoint(35, HEIGHT)
g.fillPolygon(shL)
val shR = new Polygon()
shR.addPoint(vanX + 36, horizon + 20)
shR.addPoint(vanX + 42, horizon + 20)
shR.addPoint(WIDTH - 35, HEIGHT)
shR.addPoint(WIDTH - 80, HEIGHT)
g.fillPolygon(shR)
g.setColor(GOLD)
var i = 0
while (i < 9) {
val t = i / 9.0
val yy = (horizon + 25 + t * t * (HEIGHT - horizon - 30)).toInt
val half = (3 + t * 26).toInt
g.fillRect(vanX - half, yy, half * 2, Math.max(3, (5 + t * 4).toInt))
i += 1
}
// distant skyline
var s = 0
while (s < 16) {
val bw = 35 + (s * 13) % 70
val bh = 30 + (s * 17) % 100
val bx = s * 84
g.setColor(if (s % 2 == 0) new Color(91, 98, 101) else new Color(78, 88, 91))
g.fillRect(bx, horizon - bh, bw, bh)
s += 1
}
}
def drawBattleHouse(g: Graphics2D, x: Int, y: Int, w: Int, h: Int): Unit = {
g.setColor(new Color(199, 171, 133))
g.fillRect(x, y + 15, w, h)
val roof = new Polygon()
roof.addPoint(x - 7, y + 17)
roof.addPoint(x + w / 2, y - 22)
roof.addPoint(x + w + 7, y + 17)
g.setColor(ROOF)
g.fillPolygon(roof)
g.setColor(new Color(79, 137, 177))
g.fillRect(x + 13, y + 31, 20, 18)
g.fillRect(x + w - 33, y + 31, 20, 18)
g.setColor(new Color(83, 54, 38))
g.fillRect(x + w / 2 - 13, y + h - 29, 26, 29)
}
def drawTree(g: Graphics2D, x: Int, y: Int): Unit = {
g.setColor(new Color(103, 70, 45))
g.fillRect(x - 4, y + 10, 8, 26)
g.setColor(new Color(48, 123, 64))
g.fillOval(x - 23, y - 18, 46, 45)
g.fillOval(x - 6, y - 27, 48, 48)
g.fillOval(x - 37, y - 3, 46, 42)
}
def drawBattleHUD(g: Graphics2D): Unit = {
g.setColor(new Color(8, 11, 14, 230))
g.fillRoundRect(18, 82, 330, 190, 14, 14)
drawBar(g, 35, 107, 270, 18, hp, 100, "HP")
drawBar(g, 35, 137, 270, 18, armor, 100, "ARMOR")
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 16))
g.drawString("GUN: " + gunNames(gun), 35, 187)
g.drawString("AMMO: " + ammo + " / " + gunAmmo(gun), 35, 214)
g.drawString("KILLS: " + currentKills + " LOOT: " + lootCount, 35, 241)
drawInfoBox(g, 1005, 85, 245, 235, "BATTLE CONTROLS", Array(
"WASD = move",
"SPACE = shoot",
"1-4 = guns",
"E = pickup",
"R = reload",
"F = medkit",
"M = local 2P",
"P = profile",
"ENTER = result"
))
g.setColor(GOLD)
g.setFont(new Font("SansSerif", Font.BOLD, 16))
g.drawString("POINTS: " + currentPoints, 1060, 350)
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.PLAIN, 12))
g.drawString("Enemies remaining: " + enemies.count(_.alive), 1020, 376)
}
// ----------------------------------------------------------
// RESULT / PROFILE
// ----------------------------------------------------------
def drawResult(g: Graphics2D): Unit = {
g.setColor(BG)
g.fillRect(0, 68, WIDTH, HEIGHT - 68)
g.setColor(GOLD)
g.setFont(new Font("SansSerif", Font.BOLD, 42))
g.drawString(if (gameWon) "VICTORY RESULT" else "MATCH RESULT", 415, 160)
val rows = Array(
("PLAYER", currentUsername),
("PLAYER ID", currentPlayerId),
("KILLS", currentKills.toString),
("MATCHES", totalMatches.toString),
("TOTAL POINTS", currentPoints.toString),
("COINS", currentCoins.toString),
("BEST KILLS", bestKills.toString)
)
var y = 225
var i = 0
while (i < rows.length) {
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.BOLD, 17))
g.drawString(rows(i)._1, 300, y)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.PLAIN, 17))
g.drawString(rows(i)._2, 570, y)
y += 45
i += 1
}
g.setColor(SAFE)
g.setFont(new Font("SansSerif", Font.BOLD, 22))
g.drawString("PROGRESS SAVED TO YOUR ACCOUNT", 370, 555)
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.PLAIN, 15))
g.drawString("ENTER = new journey P = profile ESC = garage", 420, 590)
}
def drawProfile(g: Graphics2D): Unit = {
g.setColor(BG)
g.fillRect(0, 68, WIDTH, HEIGHT - 68)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.BOLD, 35))
g.drawString("PLAYER PROFILE", 465, 145)
val rows = Array(
("USERNAME", currentUsername),
("PLAYER ID", currentPlayerId),
("TOTAL POINTS", currentPoints.toString),
("COINS", currentCoins.toString),
("MATCHES PLAYED", totalMatches.toString),
("BEST KILLS", bestKills.toString),
("CURRENT KILLS", currentKills.toString),
("LOCAL 2P", if (twoPlayer) "ON" else "OFF")
)
var y = 205
var i = 0
while (i < rows.length) {
g.setColor(MUTED)
g.setFont(new Font("SansSerif", Font.BOLD, 17))
g.drawString(rows(i)._1, 305, y)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.PLAIN, 17))
g.drawString(rows(i)._2, 600, y)
y += 43
i += 1
}
g.setColor(GOLD)
g.setFont(new Font("SansSerif", Font.BOLD, 19))
g.drawString("Your old points remain attached to the same saved Player ID.", 300, 575)
}
// ----------------------------------------------------------
// UI HELPERS
// ----------------------------------------------------------
def drawBar(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, value: Double, max: Double, label: String): Unit = {
g.setColor(new Color(8, 11, 14, 230))
g.fillRoundRect(x, y, w, h, 8, 8)
val ratio = Math.max(0.0, Math.min(1.0, value / max))
g.setColor(if (ratio > 0.60) SAFE else if (ratio > 0.30) GOLD else ENEMY)
g.fillRoundRect(x + 2, y + 2, ((w - 4) * ratio).toInt, h - 4, 7, 7)
g.setColor(Color.WHITE)
g.setFont(new Font("SansSerif", Font.BOLD, 10))
g.drawString(label, x + 7, y + h - 5)
}
def drawInfoBox(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, title: String, lines: Array[String]): Unit = {
g.setColor(new Color(8, 11, 14, 230))
g.fillRoundRect(x, y, w, h, 12, 12)
g.setColor(GOLD)
g.setFont(new Font("SansSerif", Font.BOLD, 14))
g.drawString(title, x + 14, y + 22)
g.setColor(TEXT)
g.setFont(new Font("SansSerif", Font.PLAIN, 12))
var yy = y + 43
var i = 0
while (i < lines.length && yy < y + h - 7) {
g.drawString(lines(i), x + 14, yy)
yy += 20
i += 1
}
}
// ----------------------------------------------------------
// LOOP
// ----------------------------------------------------------
def gameTick(): Unit = {
val now = System.currentTimeMillis()
val dt = Math.min(0.05, (now - lastTime) / 1000.0)
lastTime = now
tickNo += 1
if (messageLife > 0) messageLife -= 1
screen match {
case "CITY" => updateCity(dt)
case "FLIGHT" => updateFlight(dt)
case "LANDING" =>
landingTicks = Math.max(0, landingTicks - 1)
if (landingTicks <= 0) {
buildBattle()
screen = "BATTLE"
showMessage("LANDING COMPLETE - BATTLE STARTED")
}
case "BATTLE" => updateBattle(dt)
case _ => ()
}
repaint()
}
def updateCity(dt: Double): Unit = {
if (crashTicks > 0) {
crashTicks -= 1
if (crashTicks == 0) {
truckX = 190
truckY = 1060
truckAngle = -Math.PI / 2.0
truckSpeed = 0
truckFuel = 100
showMessage("TRUCK RESTARTED - drive carefully")
}
return
}
var move = 0.0
if (held.contains("W") || held.contains("UP")) move += 1
if (held.contains("S") || held.contains("DOWN")) move -= 0.6
if (held.contains("A") || held.contains("LEFT")) truckAngle -= 0.05
if (held.contains("D") || held.contains("RIGHT")) truckAngle += 0.05
if (move != 0 && truckFuel > 0) {
truckSpeed += move * 0.68
truckSpeed = Math.max(-3.0, Math.min(7.2, truckSpeed))
truckX += Math.cos(truckAngle) * truckSpeed * 3.0
truckY += Math.sin(truckAngle) * truckSpeed * 3.0
truckFuel -= Math.abs(truckSpeed) * 0.011
}
truckSpeed *= 0.93
truckX = Math.max(25, Math.min(WORLD_W - 25, truckX))
truckY = Math.max(25, Math.min(WORLD_H - 25, truckY))
var i = 0
while (i < traffic.length) {
val q = traffic(i)
val nx = q.x + q.dx * (0.8 + (i % 4) * 0.25)
val fixed = if (nx > WORLD_W + 50) -40 else if (nx < -50) WORLD_W + 40 else nx
traffic.update(i, q.copy(x = fixed))
i += 1
}
// Accident / collision system
if (Math.abs(truckSpeed) > 1.8) {
var k = 0
while (k < traffic.length) {
val q = traffic(k)
val dx = truckX - q.x
val dy = truckY - q.y
if (dx * dx + dy * dy < 42 * 42) {
crashTicks = 45
truckSpeed = 0
showMessage("ACCIDENT! Truck damaged - restarting...")
k = traffic.length
} else k += 1
}
}
val dx = truckX - 1080
val dy = truckY - 970
val dist = Math.sqrt(dx * dx + dy * dy)
if (dist < 260) showMessage("AIRPORT REACHED - press E")
}
def updateFlight(dt: Double): Unit = {
if (!landReady) {
val boost = if (held.contains("SPACE")) 2.0 else 1.0
planeProgress += dt * 20.0 * boost
planeAltitude = 1200 + planeProgress * 38
if (planeProgress >= 100) {
planeProgress = 100
landReady = true
showMessage("LAND READY - press L or ENTER")
}
} else {
// Keep showing a ready aircraft until the player chooses LAND.
planeAltitude = 5000
}
if (held.contains("UP") || held.contains("W")) planeAltitude += 3
if (held.contains("DOWN") || held.contains("S")) planeAltitude = Math.max(300, planeAltitude - 3)
}
def startLanding(): Unit = {
if (screen == "FLIGHT" && landReady && landingTicks <= 0) {
landingTicks = 75
screen = "LANDING"
showMessage("LANDING: aircraft descending...")
}
}
def drawLanding(g: Graphics2D): Unit = {
var y = 68
while (y < HEIGHT) {
val q = (y - 68).toDouble / (HEIGHT - 68).toDouble
val rr = (95 * (1 - q) + 210 * q).toInt
val gg = (165 * (1 - q) + 235 * q).toInt
val bb = (220 * (1 - q) + 190 * q).toInt
g.setColor(new Color(rr, gg, bb))
g.fillRect(0, y, WIDTH, 1)
y += 1
}
g.setColor(new Color(74, 121, 73))
g.fillRect(0, 510, WIDTH, 250)
g.setColor(new Color(73, 73, 78))
g.fillRect(240, 555, 800, 90)
g.setColor(Color.WHITE)
var lx = 275
while (lx < 980) {
g.fillRect(lx, 595, 55, 6)
lx += 95
}
val landingDone = Math.max(0, Math.min(75, 75 - landingTicks))
val fall = Math.min(180, landingDone * 2)
drawPlane(g, 640, 250 + fall, 0.0)
g.setColor(Color.WHITE)
g.setFont(new Font("SansSerif", Font.BOLD, 31))
g.drawString("LANDING", 520, 135)
g.setFont(new Font("SansSerif", Font.PLAIN, 17))
g.drawString("Landing gear: DOWN", 490, 170)
g.drawString("Runway: DESI DROP ZONE", 475, 200)
g.drawString("Please wait...", 555, 235)
drawBar(g, 400, 275, 480, 25, landingDone, 75, "LANDING PROGRESS")
}
def updateBattle(dt: Double): Unit = {
if (!battleActive || hp <= 0) return
battleTicks += Math.max(1, (dt * 60).toInt)
var vx = 0.0
var vy = 0.0
if (held.contains("W") || held.contains("UP")) vy -= 1
if (held.contains("S") || held.contains("DOWN")) vy += 1
if (held.contains("A") || held.contains("LEFT")) vx -= 1
if (held.contains("D") || held.contains("RIGHT")) vx += 1
val len = Math.sqrt(vx * vx + vy * vy)
if (len > 0.1) {
playerX += vx / len * 4.7
playerY += vy / len * 4.7
}
playerX = Math.max(35, Math.min(WORLD_W - 35, playerX))
playerY = Math.max(35, Math.min(WORLD_H - 35, playerY))
if (twoPlayer) updateP2()
if (held.contains("SPACE") && ammo > 0 && tickNo % fireGap(gun) == 0) shoot(1)
if (held.contains("U") && twoPlayer && tickNo % 10 == 0) shoot(2)
if (tickNo % 15 == 0) updateEnemies()
updateBullets()
autoPickup()
val zx = playerX - 900
val zy = playerY - 620
val zdist = Math.sqrt(zx * zx + zy * zy)
val zoneR = Math.max(120, 340 - battleTicks / 160)
if (zdist > zoneR && tickNo % 10 == 0) {
hp = Math.max(0, hp - 1)
showMessage("OUTSIDE SAFE ZONE")
}
if (enemies.count(_.alive) == 0) completeBattle(true)
else if (battleTicks > 42000) completeBattle(true)
}
def updateP2(): Unit = {
var vx = 0.0
var vy = 0.0
if (held.contains("I")) vy -= 1
if (held.contains("K")) vy += 1
if (held.contains("J")) vx -= 1
if (held.contains("L")) vx += 1
val len = Math.sqrt(vx * vx + vy * vy)
if (len > 0.1) {
p2X += vx / len * 4.0
p2Y += vy / len * 4.0
}
p2X = Math.max(35, Math.min(WORLD_W - 35, p2X))
p2Y = Math.max(35, Math.min(WORLD_H - 35, p2Y))
}
def updateEnemies(): Unit = {
var i = 0
while (i < enemies.length) {
val e = enemies(i)
if (e.alive) {
val dx = playerX - e.x
val dy = playerY - e.y
val d = Math.sqrt(dx * dx + dy * dy)
if (d > 60) {
enemies.update(i, e.copy(
x = e.x + dx / Math.max(1, d) * 1.25,
y = e.y + dy / Math.max(1, d) * 1.25,
cool = Math.max(0, e.cool - 1)
))
} else if (e.cool <= 0) {
val damage = 3 + rng.nextInt(6)
hp = Math.max(0, hp - Math.max(1, damage - armor / 30))
armor = Math.max(0, armor - 1)
enemies.update(i, enemies(i).copy(cool = 18 + rng.nextInt(22)))
}
if (hp <= 0) showMessage("Your player is down")
}
i += 1
}
}
def shoot(owner: Int): Unit = {
if (owner == 1 && ammo <= 0) return
val sx = if (owner == 1) playerX else p2X
val sy = if (owner == 1) playerY else p2Y
val target = enemies.filter(_.alive).sortBy { e =>
val dx = e.x - sx
val dy = e.y - sy
dx * dx + dy * dy
}.headOption
val angle = target match {
case Some(e) => Math.atan2(e.y - sy, e.x - sx)
case None => playerAngle
}
bullets += Bullet(
sx,
sy,
Math.cos(angle) * 15,
Math.sin(angle) * 15,
owner,
true
)
if (owner == 1) {
ammo -= 1
shots += 1
}
}
def updateBullets(): Unit = {
var i = bullets.length - 1
while (i >= 0) {
val b = bullets(i)
if (b.alive) {
b.x += b.vx
b.y += b.vy
if (b.x < 0 || b.y < 0 || b.x > WORLD_W || b.y > WORLD_H) b.alive = false
if (b.owner == 1 && b.alive) {
var j = 0
while (j < enemies.length) {
val e = enemies(j)
if (e.alive) {
val dx = e.x - b.x
val dy = e.y - b.y
if (dx * dx + dy * dy < 22 * 22) {
val next = e.hp - gunDamage(gun)
hits += 1
currentPoints += 10
if (next <= 0) {
currentKills += 1
currentPoints += 100
currentCoins += 25
enemies.update(j, e.copy(hp = 0, alive = false))
showMessage("Enemy defeated +100 points")
} else {
enemies.update(j, e.copy(hp = next))
showMessage("Hit +10 points")
}
b.alive = false
}
}
j += 1
}
}
}
if (!b.alive) bullets.remove(i)
i -= 1
}
}
def autoPickup(): Unit = {
var i = 0
while (i < loot.length) {
val q = loot(i)
if (!q.taken) {
val dx = q.x - playerX
val dy = q.y - playerY
if (dx * dx + dy * dy < 32 * 32 && held.contains("E")) {
loot.update(i, q.copy(taken = true))
lootCount += 1
currentPoints += 5
q.kind match {
case "AMMO" => ammo = Math.min(gunAmmo(gun), ammo + q.amount)
case "MEDKIT" => medkits += 1
case "ARMOR" => armor = Math.min(100, armor + 22)
case "SMG" => gun = 1; ammo = gunAmmo(gun)
case "RIFLE" => gun = 2; ammo = gunAmmo(gun)
case "COINS" => currentCoins += q.amount
case _ => ()
}
showMessage(q.kind + " collected")
}
}
i += 1
}
}
def completeBattle(won: Boolean): Unit = {
if (!battleActive) return
battleActive = false
gameWon = won
totalMatches += 1
bestKills = Math.max(bestKills, currentKills)
val bonus = if (won) 500 else 100
currentPoints += bonus
currentCoins += currentKills * 15
saveCurrent()
screen = "RESULT"
showMessage("Match result saved")
}
// ----------------------------------------------------------
// KEYBOARD
// ----------------------------------------------------------
override def keyPressed(e: KeyEvent): Unit = {
val c = e.getKeyCode
c match {
case KeyEvent.VK_W => held += "W"
case KeyEvent.VK_A => held += "A"
case KeyEvent.VK_S => held += "S"
case KeyEvent.VK_D => held += "D"
case KeyEvent.VK_UP => held += "UP"
case KeyEvent.VK_DOWN => held += "DOWN"
case KeyEvent.VK_LEFT => held += "LEFT"
case KeyEvent.VK_RIGHT => held += "RIGHT"
case KeyEvent.VK_SPACE => held += "SPACE"
case KeyEvent.VK_I => held += "I"
case KeyEvent.VK_J => held += "J"
case KeyEvent.VK_K => held += "K"
case KeyEvent.VK_L => held += "L"
case KeyEvent.VK_U => held += "U"
case KeyEvent.VK_E => held += "E"
case KeyEvent.VK_ENTER =>
screen match {
case "GARAGE" =>
screen = "CITY"
showMessage("City journey started")
case "RESULT" =>
buildCity()
screen = "GARAGE"
case "FLIGHT" if landReady =>
startLanding()
case "LANDING" =>
()
case "BATTLE" if hp <= 0 =>
completeBattle(false)
case _ => ()
}
case KeyEvent.VK_B =>
if (screen == "AIRPORT") screen = "CITY"
case KeyEvent.VK_F =>
if (screen == "BATTLE" && medkits > 0 && hp > 0) {
hp = Math.min(100, hp + 35)
medkits -= 1
showMessage("Medkit used")
}
case KeyEvent.VK_R =>
if (screen == "CITY") {
crashTicks = 0
truckX = 190
truckY = 1060
truckAngle = -Math.PI / 2.0
truckSpeed = 0
truckFuel = 100
showMessage("TRUCK ROUTE RESTARTED")
} else if (screen == "BATTLE") {
ammo = gunAmmo(gun)
showMessage("Reloaded")
}
case KeyEvent.VK_M =>
twoPlayer = !twoPlayer
showMessage("Local 2-player: " + (if (twoPlayer) "ON" else "OFF"))
case KeyEvent.VK_P =>
screen = "PROFILE"
case KeyEvent.VK_ESCAPE =>
if (screen == "PROFILE" || screen == "RESULT") {
screen = "GARAGE"
} else {
saveCurrent()
screen = "LOGIN"
buildLogin()
}
case KeyEvent.VK_1 | KeyEvent.VK_2 | KeyEvent.VK_3 | KeyEvent.VK_4 =>
if (screen == "BATTLE") {
val selected = c match {
case KeyEvent.VK_1 => 0
case KeyEvent.VK_2 => 1
case KeyEvent.VK_3 => 2
case _ => 3
}
if (selected == 0) {
gun = 0
ammo = gunAmmo(gun)
showMessage("Pistol equipped")
} else if (currentCoins >= gunCost(selected)) {
currentCoins -= gunCost(selected)
gun = selected
ammo = gunAmmo(gun)
showMessage(gunNames(gun) + " equipped")
} else {
showMessage("Not enough coins")
}
}
case _ => ()
}
// LAND action is evaluated after the held-key match so L works reliably.
if (c == KeyEvent.VK_L && screen == "FLIGHT" && landReady) {
startLanding()
}
// Separate transition for airport E is handled after the main match.
if (screen == "CITY" && c == KeyEvent.VK_E) {
val dx = truckX - 1080
val dy = truckY - 970
if (Math.sqrt(dx * dx + dy * dy) < 260) {
screen = "AIRPORT"
showMessage("Airport reached")
}
}
if (screen == "AIRPORT" && c == KeyEvent.VK_E) {
planeProgress = 0
planeAltitude = 1000
landReady = false
screen = "FLIGHT"
showMessage("Takeoff successful")
}
if (screen == "PROFILE" && c == KeyEvent.VK_ESCAPE) screen = "GARAGE"
repaint()
}
override def keyReleased(e: KeyEvent): Unit = {
val c = e.getKeyCode
c match {
case KeyEvent.VK_W => held -= "W"
case KeyEvent.VK_A => held -= "A"
case KeyEvent.VK_S => held -= "S"
case KeyEvent.VK_D => held -= "D"
case KeyEvent.VK_UP => held -= "UP"
case KeyEvent.VK_DOWN => held -= "DOWN"
case KeyEvent.VK_LEFT => held -= "LEFT"
case KeyEvent.VK_RIGHT => held -= "RIGHT"
case KeyEvent.VK_SPACE => held -= "SPACE"
case KeyEvent.VK_I => held -= "I"
case KeyEvent.VK_J => held -= "J"
case KeyEvent.VK_K => held -= "K"
case KeyEvent.VK_L => held -= "L"
case KeyEvent.VK_U => held -= "U"
case KeyEvent.VK_E => held -= "E"
case _ => ()
}
}
override def keyTyped(e: KeyEvent): Unit = ()
}
// ------------------------------------------------------------
// GAME VIEW SWITCH
// ------------------------------------------------------------
def showGame(): Unit = {
frame.getContentPane.removeAll()
panel = new GamePanel()
frame.setContentPane(panel)
frame.revalidate()
frame.repaint()
panel.requestFocusInWindow()
}
// ------------------------------------------------------------
// CLOSE HANDLER
// ------------------------------------------------------------
frame.addWindowListener(new WindowAdapter {
override def windowClosing(e: WindowEvent): Unit = {
saveCurrent()
System.exit(0)
}
})
// ------------------------------------------------------------
// START
// ------------------------------------------------------------
loadAccounts()
buildLogin()
frame.setVisible(true)
frame.toFront()
frame.requestFocus()
panel.requestFocusInWindow()
/*
====================================================================
DESI TRUCK BATTLE ROYALE - FEATURE INDEX
====================================================================
001 New account
002 Sign in
003 Player ID generation
004 Password hashing
005 Local persistence
006 Point persistence
007 Coin persistence
008 Match persistence
009 Best-kill persistence
010 Demo guest
011 Garage
012 Truck model
013 Cargo body
014 Cabin
015 Windows
016 Wheels
017 Plate text
018 City map
019 Indian road naming
020 Buildings
021 Building floors
022 Building windows
023 Roofs
024 Traffic cars
025 Bus
026 Auto-rickshaw style traffic
027 Truck steering
028 Truck acceleration
029 Truck braking
030 Truck fuel
031 Refuel mechanic
032 Airport marker
033 Airport terminal
034 Airport runway
035 Hangar
036 Service vehicles
037 Airplane model
038 Flight progress
039 Aircraft altitude
040 Flight controls
041 Landing-ready state
042 Landing transition
043 Battle map
044 Houses
045 House doors
046 House windows
047 Trees
048 Pond
049 Battle roads
050 Safe-zone ring
051 Dynamic safe-zone radius
052 Loot system
053 Ammo loot
054 Medkit loot
055 Armor loot
056 SMG loot
057 Rifle loot
058 Coin loot
059 Enemy bots
060 Enemy HP
061 Enemy names
062 Enemy chase
063 Enemy attack cooldown
064 Player HP
065 Player armor
066 Pistol
067 SMG
068 Rifle
069 Shotgun
070 Weapon switching
071 Weapon costs
072 Ammo counts
073 Reload
074 Shooting
075 Bullet objects
076 Bullet movement
077 Hit detection
078 Damage
079 Kill counter
080 Kill rewards
081 Loot points
082 Coin rewards
083 Victory bonus
084 Result screen
085 Profile screen
086 Same account reload
087 Local two-player
088 P1 controls
089 P2 controls
090 P2 movement
091 P2 shooting
092 Mission text
093 Status messages
094 HUD
095 Mini-map
096 Fuel bar
097 Health bar
098 Armor bar
099 Speed bar
100 Players remaining
101 Loot count
102 Current weapon
103 Profile ID
104 Total points
105 Total coins
106 Total matches
107 Best kills
108 Local-only multiplayer mode
109 Original Java2D visuals
110 No external game assets
111 No external network dependency
112 Single-file project
113 Keyboard-first controls
114 Game flow state machine
115 Account save format
116 Base64 text fields
117 SHA-256 password hash
118 Safe local save error handling
119 Window close save
120 Game timer
121 Low-allocation rendering approach
122 Traffic animation
123 Plane animation state
124 Message timer
125 Mission transitions
126 Garage reset
127 New match flow
128 Result persistence
129 Profile navigation
130 Logout flow
====================================================================
The battle gameplay is an original educational implementation inspired
by the general battle-royale genre. It does not reproduce proprietary
maps, characters, logos, weapon models, or other copyrighted assets.
====================================================================
*/
/*
============================================================
POLYGON FIX + 3D POLISH
============================================================
Graphics2D.fillPolygon requires a Polygon (or coordinate arrays),
so all perspective polygons in this file are now built explicitly
with java.awt.Polygon and addPoint. This removes the reported
Array[Point] -> Polygon type mismatch.
3D-style rendering retained:
- perspective road
- depth lane markings
- multi-storey buildings
- traffic depth
- detailed Indian cargo truck
- airplane body, wings, engines and tail
- battle arena depth and structures
============================================================
*/