Code Sketch


bhavesh shahare
By: Mhalsakant School
// ============================================================
// OPERATION SINDOOR - ULTRA CINEMATIC ARCADE EDITION
// CLEAN KOJO / SCALA VERSION
// ============================================================

cleari()
originBottomLeft()

// ============================================================
// GAME STATE
// ============================================================

var gameState = 0
var letterTextTyped = 0
var letterTimer = 0.0

// 0 = briefing
// 1 = hero entry
// 2 = 90 second defense
// 3 = cinematic finale
// 4 = final tribute

var gameClock = 90.0
var missionTime = 0.0
var finalTimer = 0.0
var finalFade = 0.0

// ============================================================
// SCORE
// ============================================================

var missilesDestroyed = 0
val targetMissiles = 20

// ============================================================
// HERO
// ============================================================

var heroX = cwidth / 2.0 - 180.0
var heroY = 35.0
var heroState = "WALKING"

// ============================================================
// TANK
// ============================================================

var tankX = cwidth / 2.0 + 90.0
val tankY = 32.0

var cannonAngle = math.Pi / 2.0
var muzzleFlashTimer = 0
var tankPulse = 0.0

// ============================================================
// MISSILE LAUNCHER
// ============================================================

var missileX = cwidth / 2.0 - 220.0
var missileY = 145.0
var missileAngle = math.Pi / 2.0

var selectedMissileType = 1

// ============================================================
// FINAL MISSILE
// ============================================================

var finaleMissileX = cwidth / 2.0 + 240.0
var finaleMissileY = cheight + 150.0
var finaleAngle = -math.Pi / 2.0
var finaleTime = 0.0

// ============================================================
// TIMERS
// ============================================================

var shootCooldown = 0
var launchCooldown = 0
var enemySpawnTimer = 0

// ============================================================
// GLOBAL ANIMATION
// ============================================================

var worldTime = 0.0
var waveAngle = 0.0
var chakraRotation = 0.0
var starDrift = 0.0
var cameraShake = 0.0
var screenFlash = 0.0

// ============================================================
// BRIEFING LETTER
// ============================================================

val officialLetterText =
  "TOP SECRET DIRECTIVE: PART 2 - THE RETURN\n" +
  "SUBJECT: ASTRONAUT DEFENSE MISSION\n\n" +
  "Welcome back, Commander.\n" +
  "A national defense alert has been activated.\n" +
  "Hold the defense line for 90 SECONDS.\n" +
  "Neutralize at least 20 incoming targets.\n\n" +
  "Use the control panel to move the tank,\n" +
  "rotate the cannon and launch interceptors.\n\n" +
  "Click CONFIRM COMMAND to begin."

// ============================================================
// AUDIO
// ============================================================

var audioEnabled = false

try {
  setNoteInstrument(Instrument.PIANO)
  audioEnabled = true
} catch {
  case _: Exception =>
}

def playFx(note: Int, duration: Int): Unit = {
  if (audioEnabled) {
    try {
      playNote(note, duration)
    } catch {
      case _: Exception =>
    }
  }
}

// ============================================================
// STAR FIELD
// ============================================================

val starCount = 280

val starX =
  Array.fill(starCount)(
    randomDouble(0.0, cwidth)
  )

val starY =
  Array.fill(starCount)(
    randomDouble(130.0, cheight)
  )

val starSize =
  Array.fill(starCount)(
    randomDouble(0.7, 2.8)
  )

// ============================================================
// PARTICLES
// ============================================================

case class DynamicParticle(
  var x: Double,
  var y: Double,
  var vx: Double,
  var vy: Double,
  var life: Double,
  col: Color,
  var size: Double
)

val particles =
  ArrayBuffer.empty[DynamicParticle]

def spawnParticle(
  x: Double,
  y: Double,
  col: Color,
  amount: Int
): Unit = {

  for (i <- 0 until amount) {

    particles.append(
      DynamicParticle(
        x,
        y,
        randomDouble(-3.0, 3.0),
        randomDouble(-2.5, 2.5),
        1.0,
        col,
        randomDouble(2.0, 6.0)
      )
    )
  }
}

def updateParticles(): Unit = {

  particles.filterInPlace(
    _.life > 0.0
  )

  particles.foreach { p =>

    p.x += p.vx
    p.y += p.vy

    p.vx *= 0.98
    p.vy *= 0.98

    p.life -= 0.035
    p.size *= 0.995
  }
}

def drawParticles(
  c: CanvasDraw
): Unit = {

  particles.foreach { p =>

    val alpha =
      math.max(
        0,
        math.min(
          255,
          (p.life * 255.0).toInt
        )
      )

    c.fill(
      p.col.getRed,
      p.col.getGreen,
      p.col.getBlue,
      alpha
    )

    c.noStroke()

    c.ellipse(
      p.x,
      p.y,
      p.size,
      p.size
    )
  }
}

// ============================================================
// EXPLOSION SPARK
// ============================================================

class ExplosionSpark(
  x0: Double,
  y0: Double,
  r: Int,
  g: Int,
  b: Int
) {

  var x = x0
  var y = y0

  var vx =
    randomDouble(-7.0, 7.0)

  var vy =
    randomDouble(-7.0, 7.0)

  var life = 1.0

  def step(): Unit = {

    x += vx
    y += vy

    vx *= 0.93
    vy *= 0.93

    life -= 0.06
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    if (life > 0.0) {

      c.fill(
        r,
        g,
        b,
        math.max(
          0,
          math.min(
            255,
            (life * 255.0).toInt
          )
        )
      )

      c.noStroke()

      c.ellipse(
        x,
        y,
        4.0,
        4.0
      )
    }
  }
}

val sparks =
  ArrayBuffer.empty[ExplosionSpark]

// ============================================================
// SHOCKWAVE
// ============================================================

class Shockwave(
  val x: Double,
  val y: Double
) {

  var radius = 5.0
  var alpha = 240.0
  var active = true

  def step(): Unit = {

    radius += 6.0
    alpha -= 14.0

    if (alpha <= 0.0) {
      active = false
    }
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    if (active) {

      c.noFill()

      c.stroke(
        255,
        180,
        70,
        math.max(
          0,
          math.min(
            255,
            alpha.toInt
          )
        )
      )

      c.strokeWeight(3)

      c.ellipse(
        x,
        y,
        radius * 2.0,
        radius * 2.0
      )
    }
  }
}

val shockwaves =
  ArrayBuffer.empty[Shockwave]

// ============================================================
// TANK SHELL
// ============================================================

class TankShell(
  var x: Double,
  var y: Double,
  val vx: Double,
  val vy: Double
) {

  var active = true

  def step(): Unit = {

    x += vx
    y += vy

    if (
      x < -50.0 ||
      x > cwidth + 50.0 ||
      y < -50.0 ||
      y > cheight + 50.0
    ) {

      active = false
    }
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    c.fill(
      255,
      200,
      70
    )

    c.noStroke()

    c.ellipse(
      x,
      y,
      9.0,
      9.0
    )
  }
}

val tankShells =
  ArrayBuffer.empty[TankShell]

// ============================================================
// ENEMY MISSILE
// ============================================================

class EnemyMissile(
  var x: Double,
  var y: Double,
  val vy: Double,
  val style: Int
) {

  var active = true
  var exploded = false

  val trail =
    ArrayBuffer.empty[(Double, Double)]

  def step(): Unit = {

    if (!exploded) {

      y += vy

      if (style == 2) {
        x +=
          math.sin(
            worldTime * 2.2 +
            y * 0.025
          ) * 1.8
      }

      if (style == 3) {
        x +=
          math.cos(
            worldTime * 2.5 +
            y * 0.03
          ) * 2.2
      }

      trail.append(
        (x, y)
      )

      if (trail.length > 12) {
        trail.remove(0)
      }

      if (y < -50.0) {
        active = false
      }
    }
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    if (!exploded) {

      var i = 0

      while (i < trail.length) {

        val p = trail(i)

        val alpha =
          math.max(
            0,
            math.min(
              255,
              (
                i.toDouble /
                trail.length.toDouble *
                150.0
              ).toInt
            )
          )

        c.fill(
          255,
          100,
          60,
          alpha
        )

        c.noStroke()

        c.ellipse(
          p._1,
          p._2,
          4.0,
          4.0
        )

        i += 1
      }

      c.pushMatrix()

      c.translate(
        x,
        y
      )

      c.rotate(
        -math.Pi / 2.0
      )

      c.fill(
        185,
        40,
        40
      )

      c.stroke(
        70,
        10,
        10
      )

      c.strokeWeight(1)

      c.rect(
        -17,
        -6,
        34,
        12
      )

      c.fill(
        255,
        160,
        70
      )

      c.noStroke()

      c.ellipse(
        18,
        0,
        11,
        11
      )

      c.popMatrix()
    }
  }
}

val enemyMissiles =
  ArrayBuffer.empty[EnemyMissile]

// ============================================================
// STRATEGIC MISSILE
// ============================================================

class StrategicMissile(
  var x: Double,
  var y: Double,
  val speed: Double,
  var angle: Double,
  val missileType: Int
) {

  var active = true

  val trail =
    ArrayBuffer.empty[(Double, Double)]

  def step(): Unit = {

    if (missileType == 2) {
      angle += 0.01
    }

    if (missileType == 3) {
      angle +=
        math.sin(
          worldTime * 4.0
        ) * 0.02
    }

    x +=
      math.cos(angle) *
      speed

    y +=
      math.sin(angle) *
      speed

    trail.append(
      (x, y)
    )

    if (trail.length > 16) {
      trail.remove(0)
    }

    if (
      x < -100.0 ||
      x > cwidth + 100.0 ||
      y < -100.0 ||
      y > cheight + 100.0
    ) {

      active = false
    }
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    var i = 0

    while (i < trail.length) {

      val p = trail(i)

      val alpha =
        math.max(
          0,
          math.min(
            255,
            (
              i.toDouble /
              trail.length.toDouble *
              160.0
            ).toInt
          )
        )

      c.fill(
        255,
        170,
        50,
        alpha
      )

      c.noStroke()

      c.ellipse(
        p._1,
        p._2,
        5.0,
        5.0
      )

      i += 1
    }

    c.pushMatrix()

    c.translate(
      x,
      y
    )

    c.rotate(
      angle
    )

    c.fill(
      220,
      225,
      235
    )

    c.stroke(
      50,
      50,
      60
    )

    c.strokeWeight(1)

    c.rect(
      -18,
      -6,
      36,
      12
    )

    c.fill(
      220,
      40,
      40
    )

    c.noStroke()

    c.ellipse(
      18,
      0,
      12,
      12
    )

    c.popMatrix()
  }
}

val strategicMissiles =
  ArrayBuffer.empty[StrategicMissile]

// ============================================================
// FIREWORK
// ============================================================

class Firework {

  val colorPalette = Array[Color](
    cm.rgb(
      255,
      153,
      51
    ),
    cm.rgb(
      255,
      255,
      255
    ),
    cm.rgb(
      18,
      136,
      7
    )
  )

  val colorIndex =
    math.min(
      2,
      math.max(
        0,
        randomDouble(
          0.0,
          3.0
        ).toInt
      )
    )

  val col: Color =
    colorPalette(colorIndex)

  val x =
    randomDouble(
      50.0,
      cwidth - 50.0
    )

  val y =
    randomDouble(
      240.0,
      cheight - 100.0
    )

  var radius = 0.0
  var active = true

  def step(): Unit = {

    radius += 5.0

    if (radius > 105.0) {
      active = false
    }
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    if (active) {

      for (i <- 0 until 28) {

        val angle =
          i.toDouble *
          math.Pi *
          2.0 /
          28.0

        val distance =
          radius *
          (
            0.65 +
            math.abs(
              math.sin(
                worldTime +
                i.toDouble
              )
            ) *
            0.35
          )

        val px =
          x +
          math.cos(angle) *
          distance

        val py =
          y +
          math.sin(angle) *
          distance

        val alpha =
          math.max(
            0,
            math.min(
              255,
              (200.0 - radius).toInt
            )
          )

        c.fill(
          col.getRed,
          col.getGreen,
          col.getBlue,
          alpha
        )

        c.noStroke()

        c.ellipse(
          px,
          py,
          3.0,
          3.0
        )
      }
    }
  }
}

val fireworks =
  ArrayBuffer.empty[Firework]

// ============================================================
// EXPLOSION EFFECT
// ============================================================

def createExplosionEffects(
  x: Double,
  y: Double
): Unit = {

  shockwaves.append(
    new Shockwave(
      x,
      y
    )
  )

  for (i <- 0 until 20) {

    sparks.append(
      new ExplosionSpark(
        x,
        y,
        255,
        random(
          90,
          220
        ),
        20
      )
    )
  }

  spawnParticle(
    x,
    y,
    cm.rgb(
      255,
      180,
      60
    ),
    15
  )

  cameraShake = 8.0
  screenFlash = 0.25

  playFx(
    55,
    60
  )
}

// ============================================================
// CONTROL SYSTEM
// ============================================================

def handleControls(): Unit = {

  if (
    gameState == 2 &&
    isMousePressed
  ) {

    val mx =
      mouseX

    val my =
      mouseY

    if (
      mx >= 20 &&
      mx <= 70 &&
      my >= 90 &&
      my <= 125
    ) {

      tankX -= 4.0

    } else if (
      mx >= 75 &&
      mx <= 125 &&
      my >= 90 &&
      my <= 125
    ) {

      tankX += 4.0

    } else if (
      mx >= 130 &&
      mx <= 180 &&
      my >= 90 &&
      my <= 125
    ) {

      cannonAngle -= 0.04

    } else if (
      mx >= 185 &&
      mx <= 235 &&
      my >= 90 &&
      my <= 125
    ) {

      cannonAngle += 0.04

    } else if (
      mx >= 240 &&
      mx <= 295 &&
      my >= 90 &&
      my <= 125
    ) {

      if (shootCooldown <= 0) {

        val sx =
          tankX +
          math.cos(
            cannonAngle
          ) * 65.0

        val sy =
          tankY +
          44.0 +
          math.sin(
            cannonAngle
          ) * 65.0

        tankShells.append(
          new TankShell(
            sx,
            sy,
            math.cos(
              cannonAngle
            ) * 10.0,
            math.sin(
              cannonAngle
            ) * 10.0
          )
        )

        shootCooldown = 12
        muzzleFlashTimer = 6
        tankPulse = 1.0

        playFx(
          65,
          40
        )
      }

    } else if (
      mx >= 300 &&
      mx <= 345 &&
      my >= 90 &&
      my <= 125
    ) {

      missileAngle -= 0.04

    } else if (
      mx >= 350 &&
      mx <= 395 &&
      my >= 90 &&
      my <= 125
    ) {

      missileAngle += 0.04

    } else if (
      mx >= 400 &&
      mx <= 445 &&
      my >= 90 &&
      my <= 125
    ) {

      selectedMissileType = 1

    } else if (
      mx >= 450 &&
      mx <= 495 &&
      my >= 90 &&
      my <= 125
    ) {

      selectedMissileType = 2

    } else if (
      mx >= 500 &&
      mx <= 545 &&
      my >= 90 &&
      my <= 125
    ) {

      selectedMissileType = 3

    } else if (
      mx >= 555 &&
      mx <= 630 &&
      my >= 90 &&
      my <= 125
    ) {

      if (launchCooldown <= 0) {

        val tipX =
          missileX +
          math.cos(
            missileAngle
          ) * 55.0

        val tipY =
          missileY +
          math.sin(
            missileAngle
          ) * 55.0

        var speed = 7.0

        if (selectedMissileType == 2) {
          speed = 6.0
        }

        if (selectedMissileType == 3) {
          speed = 8.5
        }

        strategicMissiles.append(
          new StrategicMissile(
            tipX,
            tipY,
            speed,
            missileAngle,
            selectedMissileType
          )
        )

        launchCooldown = 18

        playFx(
          60,
          50
        )
      }
    }
  }
}

// ============================================================
// COLLISION
// ============================================================

def checkCollisions(): Unit = {

  repeatFor(enemyMissiles) { enemy =>

    if (!enemy.exploded) {

      repeatFor(tankShells) { shell =>

        if (
          shell.active &&
          math.abs(shell.x - enemy.x) < 30.0 &&
          math.abs(shell.y - enemy.y) < 30.0
        ) {

          enemy.exploded = true
          shell.active = false

          missilesDestroyed += 1

          createExplosionEffects(
            enemy.x,
            enemy.y
          )
        }
      }

      repeatFor(strategicMissiles) { sm =>

        if (
          sm.active &&
          math.abs(sm.x - enemy.x) < 38.0 &&
          math.abs(sm.y - enemy.y) < 38.0
        ) {

          enemy.exploded = true
          sm.active = false

          missilesDestroyed += 1

          createExplosionEffects(
            enemy.x,
            enemy.y
          )
        }
      }
    }
  }
}

// ============================================================
// UPDATE OBJECTS
// ============================================================

def updateObjects(): Unit = {

  tankShells.filterInPlace(
    _.active
  )

  repeatFor(tankShells)(
    _.step()
  )

  strategicMissiles.filterInPlace(
    _.active
  )

  repeatFor(strategicMissiles)(
    _.step()
  )

  enemyMissiles.filterInPlace(
    _.active
  )

  repeatFor(enemyMissiles)(
    _.step()
  )

  sparks.filterInPlace(
    _.life > 0.0
  )

  repeatFor(sparks)(
    _.step()
  )

  shockwaves.filterInPlace(
    _.active
  )

  repeatFor(shockwaves)(
    _.step()
  )

  fireworks.filterInPlace(
    _.active
  )

  repeatFor(fireworks)(
    _.step()
  )
}

// ============================================================
// UPDATE GAME
// ============================================================

def updateState(): Unit = {

  worldTime += 0.025
  waveAngle += 0.08
  chakraRotation += 0.05
  starDrift += 0.01

  if (shootCooldown > 0) {
    shootCooldown -= 1
  }

  if (launchCooldown > 0) {
    launchCooldown -= 1
  }

  if (muzzleFlashTimer > 0) {
    muzzleFlashTimer -= 1
  }

  tankPulse *= 0.92
  cameraShake *= 0.90
  screenFlash *= 0.88

  updateParticles()

  // ==========================================================
  // BRIEFING
  // ==========================================================

  if (gameState == 0) {

    letterTimer += 0.04

    if (
      letterTimer >= 0.035 &&
      letterTextTyped < officialLetterText.length
    ) {

      letterTextTyped += 1
      letterTimer = 0.0

      if (
        letterTextTyped % 6 == 0
      ) {

        playFx(
          70,
          15
        )
      }
    }

    return
  }

  // ==========================================================
  // HERO ENTRY
  // ==========================================================

  if (gameState == 1) {

    if (heroX < tankX - 20.0) {

      heroX += 2.4

    } else {

      heroState = "BOARDED"

      gameState = 2

      playFx(
        72,
        100
      )
    }

    return
  }

  // ==========================================================
  // MAIN DEFENSE
  // ==========================================================

  if (gameState == 2) {

    handleControls()

    gameClock -= 0.033
    missionTime += 0.033

    updateObjects()
    checkCollisions()

    if (tankX < 60.0) {
      tankX = 60.0
    }

    if (tankX > cwidth - 60.0) {
      tankX = cwidth - 60.0
    }

    enemySpawnTimer += 1

    var spawnDelay = 72

    if (gameClock < 60.0) {
      spawnDelay = 58
    }

    if (gameClock < 30.0) {
      spawnDelay = 42
    }

    if (
      enemySpawnTimer >= spawnDelay
    ) {

      enemySpawnTimer = 0

      val ex =
        randomDouble(
          70.0,
          cwidth - 70.0
        )

      val r =
        randomDouble(
          0.0,
          1.0
        )

      val style =
        if (
          gameClock < 30.0 &&
          r > 0.55
        ) {
          3
        } else if (r > 0.25) {
          2
        } else {
          1
        }

      var speed = -0.45

      if (gameClock < 60.0) {
        speed = -0.52
      }

      if (gameClock < 30.0) {
        speed = -0.60
      }

      enemyMissiles.append(
        new EnemyMissile(
          ex,
          cheight + 25.0,
          speed,
          style
        )
      )
    }

    if (
      randomDouble(0.0, 1.0) < 0.025
    ) {

      fireworks.append(
        new Firework()
      )
    }

    if (
      randomDouble(0.0, 1.0) < 0.06
    ) {

      spawnParticle(
        randomDouble(
          0.0,
          cwidth
        ),
        randomDouble(
          140.0,
          cheight
        ),
        cm.rgb(
          80,
          180,
          255
        ),
        1
      )
    }

    if (gameClock <= 0.0) {

      gameClock = 0.0

      gameState = 3

      finaleMissileX =
        tankX + 220.0

      finaleMissileY =
        cheight + 120.0

      finaleTime = 0.0

      playFx(
        44,
        180
      )
    }

    return
  }

  // ==========================================================
  // CINEMATIC FINALE
  // ==========================================================

  if (gameState == 3) {

    finaleTime += 0.04

    finaleMissileX +=
      (
        tankX -
        finaleMissileX
      ) * 0.055

    finaleMissileY -= 6.5

    spawnParticle(
      finaleMissileX,
      finaleMissileY + 18.0,
      cm.rgb(
        255,
        100,
        20
      ),
      1
    )

    updateObjects()

    if (
      finaleMissileY <
      tankY + 30.0
    ) {

      createExplosionEffects(
        tankX,
        tankY + 35.0
      )

      gameState = 4
      finalTimer = 0.0
      finalFade = 0.0

      playFx(
        38,
        220
      )
    }

    return
  }

  // ==========================================================
  // FINAL TRIBUTE
  // ==========================================================

  if (gameState == 4) {

    finalTimer += 0.04

    finalFade += 0.006

    if (finalFade > 1.0) {
      finalFade = 1.0
    }

    updateObjects()

    return
  }
}

// ============================================================
// STAR DRAW
// ============================================================

def drawStars(
  c: CanvasDraw
): Unit = {

  for (i <- 0 until starCount) {

    val alpha =
      math.max(
        50,
        math.min(
          255,
          (
            70.0 +
            math.abs(
              math.sin(
                worldTime * 3.0 +
                i.toDouble
              )
            ) * 170.0
          ).toInt
        )
      )

    c.fill(
      255,
      255,
      255,
      alpha
    )

    c.noStroke()

    val drift =
      math.sin(
        worldTime * 0.2 +
        i.toDouble
      ) * 2.0

    c.ellipse(
      starX(i) + drift,
      starY(i),
      starSize(i),
      starSize(i)
    )
  }
}

// ============================================================
// SPACE BACKGROUND
// ============================================================

def drawSpaceBackground(
  c: CanvasDraw
): Unit = {

  c.background(
    2,
    5,
    18
  )

  drawStars(c)

  c.fill(
    30,
    70,
    150,
    25
  )

  c.noStroke()

  c.ellipse(
    170,
    cheight - 100,
    330,
    180
  )

  c.fill(
    100,
    30,
    160,
    20
  )

  c.ellipse(
    cwidth - 170,
    220,
    300,
    160
  )
}

// ============================================================
// TANK DRAW
// ============================================================

def drawTank(
  c: CanvasDraw
): Unit = {

  c.fill(
    0,
    0,
    0,
    80
  )

  c.noStroke()

  c.ellipse(
    tankX,
    tankY - 4,
    125,
    16
  )

  c.fill(
    28,
    30,
    34
  )

  c.stroke(
    10,
    10,
    12
  )

  c.strokeWeight(1)

  c.rect(
    tankX - 52,
    tankY,
    104,
    24
  )

  c.fill(
    75,
    80,
    75
  )

  for (i <- -4 to 4) {

    c.ellipse(
      tankX + i * 11,
      tankY + 12,
      9,
      9
    )
  }

  c.fill(
    55,
    88,
    48
  )

  c.rect(
    tankX - 42,
    tankY + 23,
    84,
    22
  )

  c.fill(
    45,
    75,
    40
  )

  c.ellipse(
    tankX,
    tankY + 45,
    46,
    25
  )

  val barrel = 68.0

  val tipX =
    tankX +
    math.cos(
      cannonAngle
    ) * barrel

  val tipY =
    tankY +
    45.0 +
    math.sin(
      cannonAngle
    ) * barrel

  c.stroke(
    25,
    50,
    20
  )

  c.strokeWeight(
    8.0 + tankPulse * 2.0
  )

  c.line(
    tankX,
    tankY + 45.0,
    tipX,
    tipY
  )

  if (muzzleFlashTimer > 0) {

    c.noStroke()

    c.fill(
      255,
      170,
      30,
      230
    )

    c.ellipse(
      tipX,
      tipY,
      42,
      42
    )

    c.fill(
      255,
      250,
      180,
      240
    )

    c.ellipse(
      tipX,
      tipY,
      18,
      18
    )
  }
}

// ============================================================
// LAUNCHER
// ============================================================

def drawLauncher(
  c: CanvasDraw
): Unit = {

  c.fill(
    45,
    48,
    55
  )

  c.stroke(
    18,
    22,
    25
  )

  c.strokeWeight(2)

  c.ellipse(
    missileX,
    missileY,
    58,
    58
  )

  c.pushMatrix()

  c.translate(
    missileX,
    missileY
  )

  c.rotate(
    missileAngle
  )

  c.fill(
    60,
    78,
    70
  )

  c.rect(
    -14,
    -16,
    62,
    32
  )

  c.fill(
    220,
    30,
    30
  )

  c.noStroke()

  c.rect(
    16,
    -16,
    8,
    32
  )

  c.popMatrix()
}

// ============================================================
// HERO DRAW
// ============================================================

def drawHero(
  c: CanvasDraw,
  x: Double,
  y: Double
): Unit = {

  if (heroState == "BOARDED") {
    return
  }

  val step =
    math.sin(
      worldTime * 10.0
    ) * 4.0

  c.fill(
    0,
    0,
    0,
    70
  )

  c.noStroke()

  c.ellipse(
    x,
    y - 5,
    25,
    7
  )

  c.fill(
    240,
    242,
    248
  )

  c.stroke(
    150,
    160,
    175
  )

  c.strokeWeight(1)

  c.rect(
    x - 7,
    y,
    14,
    20
  )

  c.fill(
    20,
    155,
    220
  )

  c.ellipse(
    x,
    y + 27,
    18,
    18
  )

  c.fill(
    30,
    145,
    215
  )

  c.ellipse(
    x,
    y + 27,
    11,
    7
  )

  c.stroke(
    225,
    230,
    240
  )

  c.strokeWeight(4)

  c.line(
    x - 3,
    y,
    x - 6 - step,
    y - 10
  )

  c.line(
    x + 3,
    y,
    x + 6 + step,
    y - 10
  )

  c.line(
    x - 7,
    y + 15,
    x - 13,
    y + 5
  )

  c.line(
    x + 7,
    y + 15,
    x + 13,
    y + 5
  )
}

// ============================================================
// FLAG
// ============================================================

def drawFlag(
  c: CanvasDraw
): Unit = {

  val fx =
    cwidth - 145.0

  val fy =
    cheight - 165.0

  c.stroke(
    220,
    220,
    220
  )

  c.strokeWeight(5)

  c.line(
    fx,
    fy - 100.0,
    fx,
    fy + 80.0
  )

  var x = 0.0

  while (x < 115.0) {

    val wave =
      math.sin(
        waveAngle +
        x * 0.08
      ) * 6.0

    c.strokeWeight(3)

    c.stroke(
      255,
      153,
      51
    )

    c.line(
      fx + x,
      fy + 43.0 + wave,
      fx + x,
      fy + 63.0 + wave
    )

    c.stroke(
      255,
      255,
      255
    )

    c.line(
      fx + x,
      fy + 23.0 + wave,
      fx + x,
      fy + 43.0 + wave
    )

    c.stroke(
      18,
      136,
      7
    )

    c.line(
      fx + x,
      fy + wave,
      fx + x,
      fy + 23.0 + wave
    )

    x += 3.0
  }

  c.noFill()

  c.stroke(
    0,
    0,
    150
  )

  c.strokeWeight(2)

  c.ellipse(
    fx + 58.0,
    fy + 32.0,
    17.0,
    17.0
  )
}

// ============================================================
// HUD
// ============================================================

def drawHUD(
  c: CanvasDraw
): Unit = {

  c.fill(
    8,
    14,
    30,
    235
  )

  c.stroke(
    255,
    215,
    0
  )

  c.strokeWeight(1.5)

  c.rect(
    18,
    cheight - 55,
    430,
    42
  )

  c.fill(
    255,
    215,
    0
  )

  c.text(
    s"TIME: ${gameClock.toInt}s",
    30,
    cheight - 31
  )

  c.fill(
    120,
    255,
    190
  )

  c.text(
    s"TARGETS: $missilesDestroyed / $targetMissiles",
    160,
    cheight - 31
  )

  val ratio =
    math.max(
      0.0,
      math.min(
        1.0,
        gameClock / 90.0
      )
    )

  c.fill(
    25,
    35,
    50
  )

  c.rect(
    310,
    cheight - 48,
    120,
    8
  )

  c.fill(
    255,
    153,
    51
  )

  c.rect(
    310,
    cheight - 48,
    120.0 * ratio,
    8
  )
}

// ============================================================
// CONTROL BUTTONS
// ============================================================

def drawControls(
  c: CanvasDraw
): Unit = {

  c.fill(
    12,
    18,
    32,
    235
  )

  c.stroke(
    0,
    255,
    205
  )

  c.strokeWeight(1)

  c.rect(
    15,
    88,
    665,
    48
  )

  c.fill(
    55,
    55,
    85
  )

  c.rect(
    20,
    96,
    50,
    28
  )

  c.rect(
    75,
    96,
    50,
    28
  )

  c.fill(
    255,
    255,
    255
  )

  c.text(
    "T-",
    38,
    114
  )

  c.text(
    "T+",
    94,
    114
  )

  c.fill(
    70,
    42,
    42
  )

  c.rect(
    130,
    96,
    50,
    28
  )

  c.rect(
    185,
    96,
    50,
    28
  )

  c.fill(
    255,
    255,
    255
  )

  c.text(
    "C-",
    148,
    114
  )

  c.text(
    "C+",
    203,
    114
  )

  c.fill(
    205,
    75,
    0
  )

  c.rect(
    240,
    96,
    55,
    28
  )

  c.fill(
    255,
    255,
    255
  )

  c.text(
    "FIRE",
    252,
    114
  )

  c.fill(
    40,
    90,
    90
  )

  c.rect(
    300,
    96,
    45,
    28
  )

  c.rect(
    350,
    96,
    45,
    28
  )

  c.fill(
    255,
    255,
    255
  )

  c.text(
    "M-",
    313,
    114
  )

  c.text(
    "M+",
    363,
    114
  )

  c.fill(
    if (selectedMissileType == 1) 180 else 80,
    30,
    120
  )

  c.rect(
    400,
    96,
    45,
    28
  )

  c.fill(
    if (selectedMissileType == 2) 180 else 80,
    30,
    120
  )

  c.rect(
    450,
    96,
    45,
    28
  )

  c.fill(
    if (selectedMissileType == 3) 180 else 80,
    30,
    120
  )

  c.rect(
    500,
    96,
    45,
    28
  )

  c.fill(
    255,
    255,
    255
  )

  c.text(
    "A",
    418,
    114
  )

  c.text(
    "B",
    468,
    114
  )

  c.text(
    "C",
    518,
    114
  )

  c.fill(
    145,
    0,
    180
  )

  c.rect(
    555,
    96,
    75,
    28
  )

  c.fill(
    255,
    255,
    255
  )

  c.text(
    "LAUNCH",
    565,
    114
  )
}

// ============================================================
// BRIEFING
// ============================================================

def drawBriefing(
  c: CanvasDraw
): Unit = {

  c.background(
    4,
    7,
    18
  )

  drawStars(c)

  val lx =
    cwidth / 2.0 - 270.0

  val ly =
    cheight / 2.0 - 150.0

  c.fill(
    12,
    22,
    42,
    245
  )

  c.stroke(
    0,
    255,
    205
  )

  c.strokeWeight(2)

  c.rect(
    lx,
    ly,
    540,
    320
  )

  c.fill(
    255,
    215,
    0
  )

  c.text(
    "OPERATION SINDOOR - PART 2",
    lx + 145,
    ly + 25
  )

  val shown =
    officialLetterText.substring(
      0,
      letterTextTyped
    )

  val lines =
    shown.split("\n")

  var yy =
    ly + 55.0

  for (line <- lines) {

    c.fill(
      220,
      240,
      255
    )

    c.text(
      line,
      lx + 20,
      yy
    )

    yy += 17.0
  }

  if (
    letterTextTyped >=
    officialLetterText.length
  ) {

    val bx =
      lx + 170.0

    val by =
      ly - 55.0

    c.fill(
      0,
      185,
      125
    )

    c.stroke(
      255,
      255,
      255
    )

    c.strokeWeight(2)

    c.rect(
      bx,
      by,
      200,
      36
    )

    c.fill(
      255,
      255,
      255
    )

    c.text(
      "CONFIRM COMMAND",
      bx + 32,
      by + 23
    )

    if (
      isMousePressed &&
      mouseX >= bx &&
      mouseX <= bx + 200 &&
      mouseY >= by &&
      mouseY <= by + 36
    ) {

      gameState = 1

      playFx(
        72,
        120
      )
    }
  }
}

// ============================================================
// HERO ENTRY SCREEN
// ============================================================

def drawEntry(
  c: CanvasDraw
): Unit = {

  drawSpaceBackground(c)

  drawTank(c)
  drawLauncher(c)

  drawHero(
    c,
    heroX,
    heroY
  )

  drawFlag(c)

  c.fill(
    255,
    215,
    90
  )

  c.text(
    "COMMANDER ARRIVAL",
    cwidth / 2.0 - 75,
    cheight - 35
  )
}

// ============================================================
// FINAL MISSILE
// ============================================================

def drawFinalMissile(
  c: CanvasDraw
): Unit = {

  if (gameState != 3) {
    return
  }

  c.pushMatrix()

  c.translate(
    finaleMissileX,
    finaleMissileY
  )

  c.rotate(
    finaleAngle
  )

  c.fill(
    220,
    55,
    45
  )

  c.stroke(
    80,
    20,
    20
  )

  c.strokeWeight(2)

  c.rect(
    -32,
    -11,
    64,
    22
  )

  c.fill(
    255,
    210,
    80
  )

  c.noStroke()

  c.ellipse(
    30,
    0,
    20,
    20
  )

  c.popMatrix()
}

// ============================================================
// FINAL TRIBUTE
// ============================================================

def drawFinalTribute(
  c: CanvasDraw
): Unit = {

  c.background(
    0,
    0,
    0
  )

  val a =
    math.max(
      0,
      math.min(
        255,
        (
          finalFade *
          255.0
        ).toInt
      )
    )

  for (i <- 0 until 80) {

    val sx =
      math.abs(
        math.sin(
          worldTime * 0.35 +
          i.toDouble
        )
      ) *
      (
        cwidth - 1.0
      )

    val sy =
      40.0 +
      math.abs(
        math.sin(
          i.toDouble * 1.7
        )
      ) *
      (
        cheight - 80.0
      )

    c.fill(
      255,
      255,
      255,
      math.min(
        a,
        160
      )
    )

    c.noStroke()

    c.ellipse(
      sx,
      sy,
      2.0,
      2.0
    )
  }

  c.fill(
    255,
    255,
    255,
    a
  )

  c.text(
    "THE END",
    cwidth / 2.0 - 42,
    cheight / 2.0 + 100
  )

  c.text(
    "MISSION COMPLETE",
    cwidth / 2.0 - 66,
    cheight / 2.0 + 58
  )

  c.fill(
    255,
    220,
    150,
    a
  )

  c.text(
    "PRAY FOR OUR ARMY SOLDIERS",
    cwidth / 2.0 - 125,
    cheight / 2.0 + 5
  )

  c.fill(
    235,
    235,
    235,
    a
  )

  c.text(
    "Respect. Courage. Service.",
    cwidth / 2.0 - 85,
    cheight / 2.0 - 35
  )

  c.fill(
    255,
    215,
    90,
    a
  )

  c.text(
    "JAI HIND",
    cwidth / 2.0 - 35,
    cheight / 2.0 - 85
  )

  c.strokeWeight(4)

  c.stroke(
    255,
    153,
    51,
    a
  )

  c.line(
    cwidth / 2.0 - 160,
    55,
    cwidth / 2.0 + 160,
    55
  )

  c.stroke(
    255,
    255,
    255,
    a
  )

  c.line(
    cwidth / 2.0 - 160,
    48,
    cwidth / 2.0 + 160,
    48
  )

  c.stroke(
    18,
    136,
    7,
    a
  )

  c.line(
    cwidth / 2.0 - 160,
    41,
    cwidth / 2.0 + 160,
    41
  )
}

// ============================================================
// MAIN VIEW
// ============================================================

def viewState(
  c: CanvasDraw
): Unit = {

  if (gameState == 0) {

    drawBriefing(c)

    return
  }

  if (gameState == 1) {

    drawEntry(c)

    return
  }

  if (gameState == 4) {

    drawFinalTribute(c)

    return
  }

  drawSpaceBackground(c)

  if (cameraShake > 0.5) {

    c.pushMatrix()

    c.translate(
      randomDouble(
        -cameraShake,
        cameraShake
      ),
      randomDouble(
        -cameraShake,
        cameraShake
      )
    )
  }

  drawFlag(c)
  drawLauncher(c)
  drawTank(c)

  drawParticles(c)

  repeatFor(fireworks)(
    _.view(c)
  )

  repeatFor(tankShells)(
    _.view(c)
  )

  repeatFor(strategicMissiles)(
    _.view(c)
  )

  repeatFor(enemyMissiles)(
    _.view(c)
  )

  repeatFor(sparks)(
    _.view(c)
  )

  repeatFor(shockwaves)(
    _.view(c)
  )

  if (gameState == 2) {

    drawControls(c)
    drawHUD(c)
  }

  if (gameState == 3) {

    drawFinalMissile(c)

    c.fill(
      255,
      90,
      60
    )

    c.text(
      "FINAL APPROACH",
      cwidth / 2.0 - 60,
      cheight - 40
    )
  }

  if (cameraShake > 0.5) {
    c.popMatrix()
  }

  if (screenFlash > 0.01) {

    c.fill(
      255,
      230,
      160,
      math.max(
        0,
        math.min(
          255,
          (
            screenFlash * 255.0
          ).toInt
        )
      )
    )

    c.noStroke()

    c.rect(
      0,
      0,
      cwidth,
      cheight
    )
  }
}

// ============================================================
// MAIN LOOP
// ============================================================

animateWithCanvasDraw { c =>

  updateState()

  viewState(c)
}