Code Sketch


bhavesh shahare
By: Mhalsakant School
Category: Programming
// ============================================================
// OPERATION SINDOOR - PART 2
// ULTRA LEGENDARY 90 SECOND CINEMATIC EDITION
// ASTRONAUT + ARMY HERO FINAL STORY
// CLEAN KOJO SCALA VERSION
// ============================================================

cleari()
originBottomLeft()

// ============================================================
// GAME STATES
// ============================================================

var gameState = 0

// 0 = Secret Briefing
// 1 = Astronaut + Army Entry
// 2 = 90 Second Defense
// 3 = Final Threat
// 4 = Final Reunion
// 5 = Black Fade
// 6 = THE END

// ============================================================
// STORY
// ============================================================

var storyTimer = 0.0
var missionTimer = 90.0
var endingTimer = 0.0

// ============================================================
// LETTER
// ============================================================

var letterTyped = 0
var letterTimer = 0.0

val missionLetter =
  "TOP SECRET DIRECTIVE\n" +
  "OPERATION SINDOOR - PART 2\n\n" +
  "Commander,\n\n" +
  "A critical defense alert has been activated.\n" +
  "Your mission is to protect the defense line for 90 seconds.\n" +
  "Work alongside the Army Hero and hold your position.\n\n" +
  "Click START MISSION when ready."

// ============================================================
// ASTRONAUT
// ============================================================

var astronautX = -60.0
var astronautY = 38.0
var astronautWalking = true

// ============================================================
// ARMY HERO
// ============================================================

var armyX = cwidth + 60.0
var armyY = 38.0
var armyWalking = true

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

var tankX = cwidth / 2.0
val tankY = 35.0
var cannonAngle = math.Pi / 2.0

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

var launcherX = cwidth / 2.0 - 220.0
val launcherY = 150.0
var launcherAngle = math.Pi / 2.0
var selectedType = 1

// ============================================================
// FIRE
// ============================================================

var fireCooldown = 0
var launchCooldown = 0
var muzzleFlash = 0

// ============================================================
// GENERAL ANIMATION
// ============================================================

var worldTime = 0.0
var skyTime = 0.0
var flagTime = 0.0
var particleTime = 0.0
var waveTime = 0.0

var slowMotion = false

// ============================================================
// FINALE VARIABLES
// IMPORTANT: THESE MUST BE GLOBAL
// ============================================================

var finaleX = cwidth / 2.0
var finaleY = cheight + 150.0
var finaleTime = 0.0

// ============================================================
// FINAL REUNION
// ============================================================

var finalWalk = 0.0
var finalLight = 0.0
var finalGlow = 0.0

// ============================================================
// END SCREEN
// ============================================================

var endFade = 0.0
var endStars = 0.0
var endSweep = -200.0

// ============================================================
// 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 =>
    }
  }
}

// ============================================================
// STARS
// ============================================================

val starCount = 220

val starX =
  Array.fill(starCount)(
    math.random * cwidth
  )

val starY =
  Array.fill(starCount)(
    130.0 +
    math.random * (cheight - 140.0)
  )

val starSize =
  Array.fill(starCount)(
    0.7 +
    math.random * 2.5
  )

// ============================================================
// AMBIENT GLOW PARTICLE
// ============================================================

class GlowParticle(
  x0: Double,
  y0: Double,
  rr: Int,
  gg: Int,
  bb: Int
) {

  var x = x0
  var y = y0

  var vx =
    randomDouble(
      -1.0,
      1.0
    )

  var vy =
    randomDouble(
      0.3,
      1.8
    )

  var life = 255.0

  def step(): Unit = {

    x += vx
    y += vy

    vx +=
      math.sin(
        particleTime +
        x * 0.01
      ) * 0.01

    life -= 1.8

    if (y > cheight + 20.0) {
      y = 110.0
    }

    if (x < -20.0) {
      x = cwidth + 20.0
    }

    if (x > cwidth + 20.0) {
      x = -20.0
    }
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    if (life > 0.0) {

      c.fill(
        rr,
        gg,
        bb,
        life.toInt
      )

      c.noStroke()

      val size =
        2.0 +
        math.abs(
          math.sin(
            particleTime * 2.0
          )
        ) * 2.0

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

val glowParticles =
  ArrayBuffer.empty[GlowParticle]

for (i <- 0 until 90) {

  glowParticles.append(
    new GlowParticle(
      randomDouble(0.0, cwidth),
      randomDouble(120.0, cheight),
      255,
      190,
      70
    )
  )
}

// ============================================================
// EXPLOSION PARTICLE
// ============================================================

class ExplosionParticle(
  x0: Double,
  y0: Double,
  rr: Int,
  gg: Int,
  bb: Int
) {

  var x = x0
  var y = y0

  var vx =
    randomDouble(
      -7.0,
      7.0
    )

  var vy =
    randomDouble(
      -7.0,
      7.0
    )

  var life = 255.0

  def step(): Unit = {

    x += vx
    y += vy

    vx *= 0.93
    vy *= 0.93

    life -= 9.0
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    if (life > 0.0) {

      c.fill(
        rr,
        gg,
        bb,
        life.toInt
      )

      c.noStroke()

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

val explosionParticles =
  ArrayBuffer.empty[ExplosionParticle]

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

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

  var radius = 6.0
  var alpha = 230.0
  var active = true

  def step(): Unit = {

    radius += 6.0
    alpha -= 11.0

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

  def view(
    c: CanvasDraw
  ): Unit = {

    if (active) {

      c.noFill()

      c.stroke(
        255,
        180,
        55,
        alpha.toInt
      )

      c.strokeWeight(3)

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

val shockwaves =
  ArrayBuffer.empty[Shockwave]

// ============================================================
// SHELL
// ============================================================

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

  var active = true

  def step(): Unit = {

    x += vx
    y += vy

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

      active = false
    }
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    c.fill(
      255,
      210,
      90
    )

    c.noStroke()

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

val shells =
  ArrayBuffer.empty[Shell]

// ============================================================
// ENEMY TARGET
// ============================================================

class EnemyTarget(
  var x: Double,
  var y: Double,
  val fall: Double,
  val mode: Int
) {

  var active = true
  var hit = false

  def step(): Unit = {

    if (!hit) {

      y += fall

      if (mode == 2) {

        x +=
          math.sin(
            worldTime * 2.0 +
            y * 0.02
          ) * 1.5
      }

      if (mode == 3) {

        x +=
          math.cos(
            worldTime * 2.5 +
            y * 0.03
          ) * 2.0
      }

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

  def view(
    c: CanvasDraw
  ): Unit = {

    if (!hit) {

      c.pushMatrix()

      c.translate(
        x,
        y
      )

      c.rotate(
        -math.Pi / 2.0
      )

      c.fill(
        190,
        45,
        45
      )

      c.stroke(
        70,
        15,
        15
      )

      c.strokeWeight(1)

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

      c.fill(
        60,
        60,
        60
      )

      c.noStroke()

      c.ellipse(
        17,
        0,
        10,
        10
      )

      c.popMatrix()
    }
  }
}

val enemyTargets =
  ArrayBuffer.empty[EnemyTarget]

var spawnTimer = 0

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

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

  var active = true

  def step(): Unit = {

    if (mode == 2) {
      angle += 0.009
    }

    if (mode == 3) {

      angle +=
        math.sin(
          worldTime * 3.0
        ) * 0.015
    }

    x +=
      math.cos(angle) *
      speed

    y +=
      math.sin(angle) *
      speed

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

      active = false
    }
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    c.pushMatrix()

    c.translate(
      x,
      y
    )

    c.rotate(
      angle
    )

    c.fill(
      210,
      220,
      230
    )

    c.stroke(
      50,
      50,
      65
    )

    c.strokeWeight(1)

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

    c.fill(
      225,
      45,
      45
    )

    c.noStroke()

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

    c.popMatrix()
  }
}

val strategicMissiles =
  ArrayBuffer.empty[StrategicMissile]

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

val tricolor =
  Array(
    cm.rgb(255, 153, 51),
    cm.rgb(255, 255, 255),
    cm.rgb(18, 136, 7)
  )

class FireParticle(
  var x: Double,
  var y: Double,
  val col: Color
) {

  var vx =
    randomDouble(
      -4.0,
      4.0
    )

  var vy =
    randomDouble(
      -4.0,
      4.0
    )

  var life = 255.0

  def step(): Unit = {

    x += vx
    y += vy

    vy -= 0.04

    vx *= 0.96
    vy *= 0.96

    life -= 5.0
  }

  def view(
    c: CanvasDraw
  ): Unit = {

    if (life > 0.0) {

      c.stroke(
        col
      )

      c.strokeWeight(2)

      c.point(
        x,
        y
      )
    }
  }
}

class Firework {

  val centerX =
    randomDouble(
      60.0,
      cwidth - 60.0
    )

  val centerY =
    180.0 +
    randomDouble(
      100.0,
      230.0
    )

  val color =
    tricolor(
      random(
        tricolor.length
      )
    )

  var radius = 0.0
  var active = true

  def step(): Unit = {

    radius += 7.0

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

  def view(
    c: CanvasDraw
  ): Unit = {

    if (active) {

      for (
        i <- 0 until 32
      ) {

        val a =
          i *
          math.Pi *
          2.0 /
          32.0

        val rr =
          radius *
          (
            0.55 +
            math.abs(
              math.sin(
                worldTime +
                i
              )
            ) * 0.45
          )

        c.fill(
          color.getRed,
          color.getGreen,
          color.getBlue,
          180
        )

        c.noStroke()

        c.ellipse(
          centerX +
          math.cos(a) * rr,
          centerY +
          math.sin(a) * rr,
          3,
          3
        )
      }
    }
  }
}

val fireworks =
  ArrayBuffer.empty[Firework]

// ============================================================
// IMPACT
// ============================================================

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

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

  for (
    i <- 0 until 18
  ) {

    explosionParticles.append(
      new ExplosionParticle(
        x,
        y,
        255,
        random(80, 230),
        30
      )
    )
  }

  playFx(
    52,
    90
  )
}

// ============================================================
// CONTROLS
// ============================================================

def handleControls(): Unit = {

  if (
    gameState == 2 &&
    isMousePressed
  ) {

    val mx =
      mouseX

    val my =
      mouseY

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

      tankX -= 4.0
    }

    else if (
      mx >= 80 &&
      mx <= 135 &&
      my >= 90 &&
      my <= 125
    ) {

      tankX += 4.0
    }

    else if (
      mx >= 140 &&
      mx <= 190 &&
      my >= 90 &&
      my <= 125
    ) {

      cannonAngle -= 0.04
    }

    else if (
      mx >= 195 &&
      mx <= 245 &&
      my >= 90 &&
      my <= 125
    ) {

      cannonAngle += 0.04
    }

    else if (
      mx >= 250 &&
      mx <= 305 &&
      my >= 90 &&
      my <= 125
    ) {

      if (
        fireCooldown <= 0
      ) {

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

        val sy =
          tankY +
          46.0 +
          math.sin(
            cannonAngle
          ) * 68.0

        shells.append(
          new Shell(
            sx,
            sy,
            math.cos(
              cannonAngle
            ) * 10.0,
            math.sin(
              cannonAngle
            ) * 10.0
          )
        )

        fireCooldown = 11
        muzzleFlash = 6

        playFx(
          66,
          50
        )
      }
    }

    else if (
      mx >= 315 &&
      mx <= 365 &&
      my >= 90 &&
      my <= 125
    ) {

      launcherAngle -= 0.04
    }

    else if (
      mx >= 370 &&
      mx <= 420 &&
      my >= 90 &&
      my <= 125
    ) {

      launcherAngle += 0.04
    }

    else if (
      mx >= 430 &&
      mx <= 475 &&
      my >= 90 &&
      my <= 125
    ) {

      selectedType = 1
    }

    else if (
      mx >= 480 &&
      mx <= 525 &&
      my >= 90 &&
      my <= 125
    ) {

      selectedType = 2
    }

    else if (
      mx >= 530 &&
      mx <= 575 &&
      my >= 90 &&
      my <= 125
    ) {

      selectedType = 3
    }

    else if (
      mx >= 580 &&
      mx <= 660 &&
      my >= 90 &&
      my <= 125
    ) {

      if (
        launchCooldown <= 0
      ) {

        var speed =
          7.0

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

        if (
          selectedType == 3
        ) {
          speed = 8.0
        }

        val lx =
          launcherX +
          math.cos(
            launcherAngle
          ) * 55.0

        val ly =
          launcherY +
          math.sin(
            launcherAngle
          ) * 55.0

        strategicMissiles.append(
          new StrategicMissile(
            lx,
            ly,
            speed,
            launcherAngle,
            selectedType
          )
        )

        launchCooldown = 18

        playFx(
          55,
          65
        )
      }
    }
  }
}

// ============================================================
// COLLISIONS
// ============================================================

def checkCollisions(): Unit = {

  repeatFor(
    enemyTargets
  ) { enemy =>

    if (!enemy.hit) {

      repeatFor(
        shells
      ) { shell =>

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

          enemy.hit = true
          shell.active = false

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

      repeatFor(
        strategicMissiles
      ) { missile =>

        if (
          missile.active &&
          math.abs(
            missile.x -
            enemy.x
          ) < 34.0 &&
          math.abs(
            missile.y -
            enemy.y
          ) < 34.0
        ) {

          enemy.hit = true
          missile.active = false

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

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

def updateObjects(): Unit = {

  shells.filterInPlace(
    _.active
  )

  repeatFor(
    shells
  )(
    _.step()
  )

  strategicMissiles.filterInPlace(
    _.active
  )

  repeatFor(
    strategicMissiles
  )(
    _.step()
  )

  enemyTargets.filterInPlace(
    _.active
  )

  repeatFor(
    enemyTargets
  )(
    _.step()
  )

  glowParticles.foreach(
    _.step()
  )

  explosionParticles.filterInPlace(
    _.life > 0.0
  )

  repeatFor(
    explosionParticles
  )(
    _.step()
  )

  shockwaves.filterInPlace(
    _.active
  )

  repeatFor(
    shockwaves
  )(
    _.step()
  )

  fireworks.filterInPlace(
    _.active
  )

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

// ============================================================
// MAIN UPDATE
// ============================================================

def updateState(): Unit = {

  worldTime += 0.04
  skyTime += 0.025
  flagTime += 0.08
  particleTime += 0.06
  waveTime += 0.04

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

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

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

  // ----------------------------------------------------------
  // LETTER
  // ----------------------------------------------------------

  if (
    gameState == 0
  ) {

    letterTimer += 0.04

    if (
      letterTimer >= 0.035 &&
      letterTyped < missionLetter.length
    ) {

      letterTyped += 1
      letterTimer = 0.0
    }

    return
  }

  // ----------------------------------------------------------
  // ENTRY
  // ----------------------------------------------------------

  if (
    gameState == 1
  ) {

    if (
      astronautX <
      cwidth / 2.0 - 55.0
    ) {

      astronautX += 2.5
    }
    else {

      astronautWalking = false
    }

    if (
      armyX >
      cwidth / 2.0 + 55.0
    ) {

      armyX -= 2.5
    }
    else {

      armyWalking = false
    }

    if (
      astronautWalking == false &&
      armyWalking == false
    ) {

      gameState = 2

      playFx(
        72,
        140
      )
    }

    return
  }

  // ----------------------------------------------------------
  // 90 SECOND MISSION
  // ----------------------------------------------------------

  if (
    gameState == 2
  ) {

    handleControls()

    missionTimer -= 0.033

    updateObjects()

    checkCollisions()

    // Wave difficulty increases with time
    spawnTimer += 1

    var spawnDelay = 72

    if (
      missionTimer < 60.0
    ) {

      spawnDelay = 58
    }

    if (
      missionTimer < 30.0
    ) {

      spawnDelay = 44
    }

    if (
      spawnTimer >= spawnDelay
    ) {

      spawnTimer = 0

      val ex =
        randomDouble(
          80.0,
          cwidth - 80.0
        )

      var mode = 1

      if (
        randomDouble(1.0) > 0.65
      ) {

        mode = 2
      }

      if (
        missionTimer < 35.0 &&
        randomDouble(1.0) > 0.65
      ) {

        mode = 3
      }

      var speed =
        -0.45

      if (
        missionTimer < 30.0
      ) {

        speed = -0.52
      }

      enemyTargets.append(
        new EnemyTarget(
          ex,
          cheight + 25.0,
          speed,
          mode
        )
      )
    }

    // Fireworks
    if (
      randomDouble(1.0) < 0.025
    ) {

      fireworks.append(
        new Firework()
      )
    }

    // Tank limits
    if (
      tankX < 60.0
    ) {

      tankX = 60.0
    }

    if (
      tankX >
      cwidth - 60.0
    ) {

      tankX =
        cwidth - 60.0
    }

    if (
      cannonAngle < 0.20
    ) {

      cannonAngle =
        0.20
    }

    if (
      cannonAngle >
      math.Pi - 0.20
    ) {

      cannonAngle =
        math.Pi - 0.20
    }

    if (
      launcherAngle < -math.Pi
    ) {

      launcherAngle =
        -math.Pi
    }

    if (
      launcherAngle > math.Pi
    ) {

      launcherAngle =
        math.Pi
    }

    // 90 seconds completed
    if (
      missionTimer <= 0.0
    ) {

      missionTimer = 0.0

      gameState = 3

      finaleX =
        tankX

      finaleY =
        cheight + 150.0

      finaleTime = 0.0

      playFx(
        42,
        140
      )
    }

    return
  }

  // ----------------------------------------------------------
  // FINAL THREAT
  // ----------------------------------------------------------

  if (
    gameState == 3
  ) {

    finaleTime += 0.04

    finaleX +=
      (
        tankX -
        finaleX
      ) * 0.055

    finaleY -=
      6.0

    if (
      randomDouble(1.0) < 0.14
    ) {

      explosionParticles.append(
        new ExplosionParticle(
          finaleX,
          finaleY + 18.0,
          255,
          90,
          20
        )
      )
    }

    updateObjects()

    if (
      finaleY <
      tankY + 25.0
    ) {

      createImpact(
        tankX,
        tankY + 35.0
      )

      slowMotion = true

      gameState = 4

      endingTimer = 0.0

      playFx(
        38,
        220
      )
    }

    return
  }

  // ----------------------------------------------------------
  // FINAL REUNION
  // ----------------------------------------------------------

  if (
    gameState == 4
  ) {

    endingTimer += 0.04
    finalLight += 0.04
    finalGlow += 0.05

    updateObjects()

    if (
      finalWalk < 62.0
    ) {

      finalWalk += 0.8
    }

    if (
      endingTimer >= 7.0
    ) {

      gameState = 5

      endFade = 0.0
      endStars = 0.0
      endSweep = -200.0
    }

    return
  }

  // ----------------------------------------------------------
  // BLACK FADE
  // ----------------------------------------------------------

  if (
    gameState == 5
  ) {

    endFade += 0.012
    endStars += 0.01
    endSweep += 2.0

    if (
      endFade > 1.0
    ) {

      endFade = 1.0
    }

    if (
      endSweep >
      cwidth + 250.0
    ) {

      endSweep = -250.0
    }

    if (
      endFade >= 1.0
    ) {

      gameState = 6
    }

    return
  }

  // ----------------------------------------------------------
  // THE END
  // ----------------------------------------------------------

  if (
    gameState == 6
  ) {

    endStars += 0.008
    endSweep += 2.0

    if (
      endSweep >
      cwidth + 250.0
    ) {

      endSweep = -250.0
    }

    return
  }
}

// ============================================================
// BACKGROUND
// ============================================================

def drawBackground(
  c: CanvasDraw
): Unit = {

  c.background(
    5,
    8,
    18
  )

  for (
    i <- 0 until starCount
  ) {

    val alpha =
      (
        60 +
        math.abs(
          math.sin(
            skyTime * 3.0 +
            i
          )
        ) * 180
      ).toInt

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

    c.noStroke()

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

  c.fill(
    28,
    90,
    48
  )

  c.rect(
    0,
    0,
    cwidth,
    105
  )

  c.fill(
    30,
    105,
    200
  )

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

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

def drawFlag(
  c: CanvasDraw
): Unit = {

  val fx =
    cwidth - 165.0

  val fy =
    cheight - 160.0

  val fw =
    135.0

  val fh =
    22.0

  c.stroke(
    220,
    220,
    220
  )

  c.strokeWeight(5)

  c.line(
    fx,
    fy - 120.0,
    fx,
    fy + 70.0
  )

  var x = 0.0

  while (
    x < fw
  ) {

    val offset =
      math.sin(
        flagTime +
        x * 0.035
      ) * 7.0

    c.strokeWeight(4)

    c.stroke(
      255,
      153,
      51
    )

    c.line(
      fx + x,
      fy + 44.0 + offset,
      fx + x,
      fy + 66.0 + offset
    )

    c.stroke(
      255,
      255,
      255
    )

    c.line(
      fx + x,
      fy + 22.0 + offset,
      fx + x,
      fy + 44.0 + offset
    )

    c.stroke(
      18,
      136,
      7
    )

    c.line(
      fx + x,
      fy + offset,
      fx + x,
      fy + 22.0 + offset
    )

    x += 4.0
  }

  val chakraX =
    fx +
    fw / 2.0

  val chakraY =
    fy +
    33.0 +
    math.sin(
      flagTime
    ) * 7.0

  c.noFill()

  c.stroke(
    0,
    0,
    140
  )

  c.strokeWeight(2)

  c.ellipse(
    chakraX,
    chakraY,
    18,
    18
  )
}

// ============================================================
// ASTRONAUT DRAW
// ============================================================

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

  var leg = 0.0

  if (astronautWalking) {

    leg =
      math.sin(
        worldTime * 10.0
      ) * 3.5
  }

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

  c.noStroke()

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

  // body
  c.fill(
    235,
    238,
    245
  )

  c.stroke(
    120,
    140,
    165
  )

  c.strokeWeight(2)

  c.rect(
    x - 7.0,
    y,
    14,
    21
  )

  // helmet
  c.ellipse(
    x,
    y + 28.0,
    20,
    20
  )

  // visor
  c.fill(
    20,
    150,
    220
  )

  c.noStroke()

  c.ellipse(
    x,
    y + 28.0,
    13,
    9
  )

  // legs
  c.stroke(
    215,
    220,
    228
  )

  c.strokeWeight(4)

  c.line(
    x - 3,
    y,
    x - 6.0 - leg,
    y - 11.0
  )

  c.line(
    x + 3,
    y,
    x + 6.0 + leg,
    y - 11.0
  )

  // arms
  c.line(
    x - 7,
    y + 17,
    x - 13,
    y + 7
  )

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

// ============================================================
// ARMY PERSON DRAW
// ============================================================

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

  var walk = 0.0

  if (armyWalking) {

    walk =
      math.sin(
        worldTime * 10.0
      ) * 4.0
  }

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

  c.noStroke()

  c.ellipse(
    x,
    y - 7.0,
    28,
    7
  )

  // boots
  c.fill(
    15,
    15,
    18
  )

  c.rect(
    x - 8.0 - walk,
    y - 10.0,
    7,
    4
  )

  c.rect(
    x + 1.0 + walk,
    y - 10.0,
    7,
    4
  )

  // uniform
  c.fill(
    70,
    100,
    50
  )

  c.stroke(
    35,
    55,
    30
  )

  c.strokeWeight(1)

  c.rect(
    x - 10.0,
    y,
    20,
    28
  )

  // head
  c.fill(
    205,
    155,
    120
  )

  c.noStroke()

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

  // cap
  c.fill(
    50,
    75,
    35
  )

  c.rect(
    x - 10.0,
    y + 47.0,
    20,
    6
  )

  // legs
  c.stroke(
    60,
    85,
    45
  )

  c.strokeWeight(5)

  c.line(
    x - 5.0,
    y,
    x - 7.0 - walk,
    y - 12.0
  )

  c.line(
    x + 5.0,
    y,
    x + 7.0 + walk,
    y - 12.0
  )

  // arms
  c.line(
    x - 10,
    y + 22,
    x - 16,
    y + 8
  )

  c.line(
    x + 10,
    y + 22,
    x + 16,
    y + 8
  )
}

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

def drawTank(
  c: CanvasDraw
): Unit = {

  if (
    gameState == 4 ||
    gameState == 5 ||
    gameState == 6
  ) {

    return
  }

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

  c.noStroke()

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

  // tracks
  c.fill(
    25,
    27,
    30
  )

  c.stroke(
    10,
    10,
    12
  )

  c.strokeWeight(2)

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

  // wheels
  c.fill(
    75,
    80,
    75
  )

  for (
    i <- -4 to 4
  ) {

    c.ellipse(
      tankX + i * 11.0,
      tankY + 12.0,
      9,
      9
    )
  }

  // hull
  c.fill(
    55,
    88,
    47
  )

  c.rect(
    tankX - 42.0,
    tankY + 24.0,
    84,
    22
  )

  // turret
  c.fill(
    43,
    74,
    40
  )

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

  // cannon
  val barrel = 68.0

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

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

  c.stroke(
    25,
    50,
    20
  )

  c.strokeWeight(8)

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

  if (
    muzzleFlash > 0
  ) {

    c.noStroke()

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

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

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

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

// ============================================================
// LAUNCHER DRAW
// ============================================================

def drawLauncher(
  c: CanvasDraw
): Unit = {

  c.fill(
    45,
    48,
    55
  )

  c.stroke(
    20,
    22,
    26
  )

  c.strokeWeight(2)

  c.ellipse(
    launcherX,
    launcherY,
    58,
    58
  )

  c.pushMatrix()

  c.translate(
    launcherX,
    launcherY
  )

  c.rotate(
    launcherAngle
  )

  c.fill(
    62,
    78,
    70
  )

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

  c.fill(
    220,
    30,
    30
  )

  c.noStroke()

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

  c.popMatrix()
}

// ============================================================
// CONTROLS
// ============================================================

def drawControls(
  c: CanvasDraw
): Unit = {

  c.fill(
    10,
    16,
    30,
    240
  )

  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(
    "TANK-",
    29,
    114
  )

  c.text(
    "TANK+",
    84,
    114
  )

  c.fill(
    75,
    42,
    42
  )

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

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

  c.fill(
    255,
    255,
    255
  )

  c.text(
    "CANN-",
    136,
    114
  )

  c.text(
    "CANN+",
    191,
    114
  )

  c.fill(
    205,
    70,
    0
  )

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

  c.fill(
    255,
    255,
    255
  )

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

  c.fill(
    40,
    90,
    90
  )

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

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

  c.fill(
    255,
    255,
    255
  )

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

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

  c.fill(
    70,
    30,
    110
  )

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

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

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

  c.fill(
    255,
    255,
    255
  )

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

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

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

  c.fill(
    145,
    0,
    180
  )

  c.rect(
    565,
    96,
    90,
    28
  )

  c.fill(
    255,
    255,
    255
  )

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

// ============================================================
// 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 - 58,
    420,
    44
  )

  c.fill(
    255,
    215,
    0
  )

  c.text(
    s"MISSION TIME: ${missionTimer.toInt}s",
    30,
    cheight - 34
  )

  c.fill(
    120,
    255,
    200
  )

  c.text(
    "90 SECOND DEFENSE",
    190,
    cheight - 34
  )

  // progress
  c.fill(
    30,
    40,
    60
  )

  c.rect(
    450,
    cheight - 50,
    190,
    14
  )

  val progress =
    missionTimer / 90.0

  c.fill(
    255,
    153,
    51
  )

  c.rect(
    450,
    cheight - 50,
    190 * progress,
    14
  )
}

// ============================================================
// LETTER
// ============================================================

def drawLetter(
  c: CanvasDraw
): Unit = {

  c.background(
    3,
    6,
    14
  )

  val lx =
    cwidth / 2.0 - 270.0

  val ly =
    cheight / 2.0 - 150.0

  c.fill(
    15,
    25,
    45,
    250
  )

  c.stroke(
    0,
    255,
    200
  )

  c.strokeWeight(
    2.5
  )

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

  c.fill(
    255,
    215,
    0
  )

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

  val shown =
    missionLetter.substring(
      0,
      letterTyped
    )

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

  var yy =
    ly + 55.0

  for (
    line <- lines
  ) {

    c.fill(
      225,
      240,
      255
    )

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

    yy += 17.0
  }

  if (
    letterTyped >=
    missionLetter.length
  ) {

    val bx =
      lx + 170.0

    val by =
      ly - 55.0

    c.fill(
      0,
      180,
      120
    )

    c.stroke(
      255,
      255,
      255
    )

    c.strokeWeight(2)

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

    c.fill(
      255,
      255,
      255
    )

    c.text(
      "START MISSION",
      bx + 47,
      by + 23
    )

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

      gameState = 1

      playFx(
        72,
        120
      )
    }
  }
}

// ============================================================
// FINAL THREAT DRAW
// ============================================================

def drawFinalThreat(
  c: CanvasDraw
): Unit = {

  if (
    gameState != 3
  ) {

    return
  }

  for (
    i <- 0 until 15
  ) {

    c.fill(
      255,
      80,
      20,
      120 - i * 6
    )

    c.noStroke()

    c.ellipse(
      finaleX,
      finaleY + i * 11,
      10 - i * 0.4,
      10 - i * 0.4
    )
  }

  c.pushMatrix()

  c.translate(
    finaleX,
    finaleY
  )

  c.rotate(
    -math.Pi / 2.0
  )

  c.fill(
    240,
    70,
    45
  )

  c.stroke(
    100,
    20,
    20
  )

  c.strokeWeight(2)

  c.rect(
    -30,
    -10,
    60,
    20
  )

  c.fill(
    255,
    225,
    100
  )

  c.noStroke()

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

  c.popMatrix()
}

// ============================================================
// FINAL REUNION
// ============================================================

def drawFinalReunion(
  c: CanvasDraw
): Unit = {

  c.background(
    5,
    6,
    10
  )

  val glow =
    25 +
    math.abs(
      math.sin(
        finalGlow
      )
    ) * 35

  c.fill(
    255,
    185,
    80,
    glow.toInt
  )

  c.noStroke()

  c.ellipse(
    cwidth / 2.0,
    320,
    430,
    260
  )

  // tricolor horizon
  c.strokeWeight(
    5
  )

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

  c.line(
    100,
    120,
    cwidth - 100,
    120
  )

  c.stroke(
    255,
    255,
    255,
    150
  )

  c.line(
    100,
    112,
    cwidth - 100,
    112
  )

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

  c.line(
    100,
    104,
    cwidth - 100,
    104
  )

  val ax =
    cwidth / 2.0 -
    65.0 +
    finalWalk

  val bx =
    cwidth / 2.0 +
    65.0 -
    finalWalk

  if (
    finalWalk < 62.0
  ) {

    drawAstronaut(
      c,
      ax,
      135
    )

    drawArmyPerson(
      c,
      bx,
      135
    )
  }
  else {

    drawAstronaut(
      c,
      cwidth / 2.0 - 18.0,
      135
    )

    drawArmyPerson(
      c,
      cwidth / 2.0 + 18.0,
      135
    )

    c.fill(
      255,
      255,
      255,
      80
    )

    c.noStroke()

    c.ellipse(
      cwidth / 2.0,
      225,
      95,
      95
    )

    c.fill(
      255,
      225,
      150,
      200
    )

    c.text(
      "MISSION OVER",
      cwidth / 2.0 - 55,
      300
    )

    c.fill(
      245,
      245,
      245
    )

    c.text(
      "A final salute in silence.",
      cwidth / 2.0 - 70,
      275
    )
  }
}

// ============================================================
// BLACK FADE
// ============================================================

def drawFade(
  c: CanvasDraw
): Unit = {

  c.background(
    0,
    0,
    0
  )

  val alpha =
    (
      endFade * 255.0
    ).toInt

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

  c.text(
    "THE END",
    cwidth / 2.0 - 40.0,
    cheight / 2.0 + 60.0
  )

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

  c.text(
    "THE END OF THE ASTRONAUT",
    cwidth / 2.0 - 120.0,
    cheight / 2.0 + 20.0
  )

  c.text(
    "AND THE ARMY HERO",
    cwidth / 2.0 - 105.0,
    cheight / 2.0 - 10.0
  )
}

// ============================================================
// FINAL END SCREEN
// ============================================================

def drawEnd(
  c: CanvasDraw
): Unit = {

  c.background(
    0,
    0,
    0
  )

  // stars
  for (
    i <- 0 until 90
  ) {

    val sx =
      (
        i * 83.0 +
        endStars *
        (8.0 + i % 4)
      ) %
      (cwidth + 30.0)

    val sy =
      55.0 +
      (
        i * 47
      ) %
      (cheight - 120.0)

    val sa =
      (
        60 +
        math.abs(
          math.sin(
            endStars +
            i
          )
        ) * 170
      ).toInt

    c.fill(
      255,
      255,
      255,
      sa
    )

    c.noStroke()

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

  // light sweep
  c.fill(
    255,
    255,
    255,
    16
  )

  c.rect(
    endSweep,
    0,
    85,
    cheight
  )

  // THE END
  c.fill(
    255,
    255,
    255
  )

  c.text(
    "THE END",
    cwidth / 2.0 - 42.0,
    cheight / 2.0 + 100.0
  )

  // Story title
  c.text(
    "THE END OF THE ASTRONAUT",
    cwidth / 2.0 - 125.0,
    cheight / 2.0 + 48.0
  )

  c.text(
    "AND THE ARMY HERO",
    cwidth / 2.0 - 105.0,
    cheight / 2.0 + 18.0
  )

  // Tribute
  c.fill(
    255,
    255,
    255
  )

  c.text(
    "PRAY FOR OUR ARMY SOLDIERS",
    cwidth / 2.0 - 138.0,
    cheight / 2.0 - 30.0
  )

  c.fill(
    225,
    225,
    225
  )

  c.text(
    "We remember their courage, service and sacrifice.",
    cwidth / 2.0 - 155.0,
    cheight / 2.0 - 68.0
  )

  c.fill(
    255,
    255,
    255
  )

  c.text(
    "JAI HIND",
    cwidth / 2.0 - 35.0,
    cheight / 2.0 - 115.0
  )

  c.text(
    "SALUTE TO OUR SOLDIERS",
    cwidth / 2.0 - 83.0,
    cheight / 2.0 - 152.0
  )

  // tricolor
  c.strokeWeight(4)

  c.stroke(
    255,
    153,
    51
  )

  c.line(
    cwidth / 2.0 - 155.0,
    75.0,
    cwidth / 2.0 + 155.0,
    75.0
  )

  c.stroke(
    255,
    255,
    255
  )

  c.line(
    cwidth / 2.0 - 155.0,
    68.0,
    cwidth / 2.0 + 155.0,
    68.0
  )

  c.stroke(
    18,
    136,
    7
  )

  c.line(
    cwidth / 2.0 - 155.0,
    61.0,
    cwidth / 2.0 + 155.0,
    61.0
  )
}

// ============================================================
// VIEW STATE
// ============================================================

def viewState(
  c: CanvasDraw
): Unit = {

  // ----------------------------------------------------------
  // LETTER
  // ----------------------------------------------------------

  if (
    gameState == 0
  ) {

    drawLetter(c)

    return
  }

  // ----------------------------------------------------------
  // FINAL REUNION
  // ----------------------------------------------------------

  if (
    gameState == 4
  ) {

    drawFinalReunion(c)

    return
  }

  // ----------------------------------------------------------
  // BLACK FADE
  // ----------------------------------------------------------

  if (
    gameState == 5
  ) {

    drawFade(c)

    return
  }

  // ----------------------------------------------------------
  // FINAL SCREEN
  // ----------------------------------------------------------

  if (
    gameState == 6
  ) {

    drawEnd(c)

    return
  }

  // ----------------------------------------------------------
  // NORMAL BACKGROUND
  // ----------------------------------------------------------

  drawBackground(c)

  drawFlag(c)

  repeatFor(
    glowParticles
  )(
    _.view(c)
  )

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

  // ----------------------------------------------------------
  // ENTRY
  // ----------------------------------------------------------

  if (
    gameState == 1
  ) {

    drawAstronaut(
      c,
      astronautX,
      astronautY
    )

    drawArmyPerson(
      c,
      armyX,
      armyY
    )

    c.fill(
      255,
      215,
      90
    )

    c.text(
      "ASTRONAUT + ARMY DEFENSE TEAM",
      cwidth / 2.0 - 105.0,
      cheight - 55.0
    )

    return
  }

  // ----------------------------------------------------------
  // MAIN MISSION
  // ----------------------------------------------------------

  drawLauncher(c)

  drawTank(c)

  repeatFor(
    shells
  )(
    _.view(c)
  )

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

  repeatFor(
    enemyTargets
  )(
    _.view(c)
  )

  repeatFor(
    explosionParticles
  )(
    _.view(c)
  )

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

  if (
    gameState == 2
  ) {

    drawControls(c)

    drawHUD(c)
  }

  if (
    gameState == 3
  ) {

    drawFinalThreat(c)

    c.fill(
      255,
      80,
      50
    )

    c.text(
      "FINAL THREAT",
      cwidth / 2.0 - 45.0,
      cheight - 45.0
    )
  }
}

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

animateWithCanvasDraw { c =>

  updateState()

  viewState(c)
}