Code Sketch


Bixuu spring season event 7
By: Mhalsakant School
Category: Programming
import java.awt.{BasicStroke, Color, Dimension, Font, GradientPaint, Graphics, Graphics2D, Polygon, RenderingHints, Rectangle}
import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, MouseAdapter, MouseEvent}
import java.awt.geom.{Ellipse2D, RoundRectangle2D}
import java.util.Locale
import java.util.prefs.Preferences
import javax.swing.{JFrame, JOptionPane, JPanel, SwingUtilities, Timer, WindowConstants}
import scala.collection.mutable.ArrayBuffer
import scala.util.Random
import scala.util.Try

object AlienTidalSurge {
  private val AppTitle = "ALIEN TIDAL SURGE - EVENT 7"
  private val WindowWidth = 1000
  private val WindowHeight = 700
  private val BackgroundTop = new Color(8, 18, 54)
  private val BackgroundBottom = new Color(15, 82, 112)
  private val Aqua = new Color(72, 235, 224)
  private val BrightBlue = new Color(91, 177, 255)
  private val Coral = new Color(255, 133, 116)
  private val Gold = new Color(255, 211, 95)
  private val White = new Color(239, 250, 255)
  private val PanelColor = new Color(5, 25, 53, 225)

  private case class UiButton(rect: Rectangle, label: String, action: () => Unit, enabled: Boolean)

  private class Alien(var x: Double, var y: Double, val boss: Boolean, var hp: Int, val phase: Double) {
    var alive = true
    def radius: Double = if (boss)  thirtyEight else 20.0
    private val thirtyEight = 38.0
  }

  private class Pulse(var x: Double, var y: Double, val vx: Double, val vy: Double) {
    var life = 75
  }

  private class Crystal(var x: Double, var y: Double) {
    var phase = 0.0
  }

  private class GamePanel(val playerName: String) extends JPanel {
    private val safeName = playerName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "_")
    private val prefs = Preferences.userRoot().node("BixuuGames/AlienTidalSurge/players/" + safeName)
    private val random = new Random()
    private val buttons = ArrayBuffer[UiButton]()
    private val aliens = ArrayBuffer[Alien]()
    private val pulses = ArrayBuffer[Pulse]()
    private val crystals = ArrayBuffer[Crystal]()

    private var screen = "menu"
    private var level = 1
    private var selectedLevel = 1
    private var highestLevel = math.max(1, math.min(10, prefs.getInt("highestLevel", 1)))
    private var surgeCoins = math.max(0, prefs.getInt("surgeCoins", 0))
    private var bestScore = math.max(0, prefs.getInt("bestScore", 0))
    private var suitOwned = prefs.getBoolean("aquaSuit", false)
    private var selectedSuit = math.max(0, math.min(2, prefs.getInt("selectedSuit", 0)))

    private var playerX = 500.0
    private var playerY = 500.0
    private var up = false
    private var down = false
    private var left = false
    private var right = false
    private var paused = false
    private var lives = 3
    private var shieldTicks = 0
    private var invulnerableTicks = 0
    private var magnetTicks = 0
    private var attackCooldown = 0
    private var defeated = 0
    private var enemyGoal = 4
    private var score = 0
    private var tick = 0
    private var waveY = WindowHeight + 80.0
    private var waveActive = false
    private var waveCooldown = 300
    private var waveAlreadyHit = false
    private var message = "Explore the tide planet and collect energy crystals!"
    private var messageTicks = 220

    setPreferredSize(new Dimension(WindowWidth, WindowHeight))
    setFocusable(true)
    setBackground(BackgroundBottom)

    private val gameTimer = new Timer(16, new ActionListener {
      override def actionPerformed(event: ActionEvent): Unit = {
        updateGame()
        repaint()
      }
    })

    def startLoop(): Unit = gameTimer.start()

    private def saveProgress(): Unit = {
      Try {
        prefs.putInt("highestLevel", highestLevel)
        prefs.putInt("surgeCoins", surgeCoins)
        prefs.putInt("bestScore", bestScore)
        prefs.putBoolean("aquaSuit", suitOwned)
        prefs.putInt("selectedSuit", selectedSuit)
        prefs.flush()
      }
    }

    private def setMessage(value: String): Unit = {
      message = value
      messageTicks = 180
    }

    private def suitColor(index: Int): Color = {
      index match {
        case 0 => new Color(90, 245, 222)
        case 1 => new Color(255, 191, 104)
        case _ => new Color(179, 155, 255)
      }
    }

    private def suitName(index: Int): String = {
      index match {
        case 0 => "TIDE RUNNER"
        case 1 => "SOLAR EXPLORER"
        case _ => "NEBULA GUARD"
      }
    }

    private def addButton(g: Graphics2D, x: Int, y: Int, w: Int, h: Int,
                          label: String, enabled: Boolean = true)(action: => Unit): Unit = {
      val rect = new Rectangle(x, y, w, h)
      buttons += UiButton(rect, label, () => action, enabled)
      val fill = if (enabled) new Color(9, 103, 128, 238) else new Color(65, 83, 101, 205)
      g.setColor(fill)
      g.fill(new RoundRectangle2D.Double(x, y, w, h, 22, 22))
      g.setColor(if (enabled) Aqua else new Color(170, 190, 200))
      g.setStroke(new BasicStroke(2.0f))
      g.draw(new RoundRectangle2D.Double(x, y, w, h, 22, 22))
      g.setFont(new Font("SansSerif", Font.BOLD, 15))
      val fm = g.getFontMetrics
      g.setColor(White)
      g.drawString(label, x + (w - fm.stringWidth(label)) / 2, y + (h + fm.getAscent - fm.getDescent) / 2)
    }

    private def drawText(g: Graphics2D, value: String, x: Int, y: Int, size: Int,
                         color: Color, bold: Boolean = false): Unit = {
      g.setFont(new Font("SansSerif", if (bold) Font.BOLD else Font.PLAIN, size))
      g.setColor(color)
      g.drawString(value, x, y)
    }

    private def drawPanel(g: Graphics2D, x: Int, y: Int, w: Int, h: Int): Unit = {
      g.setColor(PanelColor)
      g.fill(new RoundRectangle2D.Double(x, y, w, h, 24, 24))
      g.setColor(new Color(92, 238, 226, 190))
      g.setStroke(new BasicStroke(2.0f))
      g.draw(new RoundRectangle2D.Double(x, y, w, h, 24, 24))
    }

    private def drawStar(g: Graphics2D, x: Int, y: Int, radius: Int, color: Color): Unit = {
      val points = new Polygon()
      var i = 0
      while (i < 10) {
        val angle = -math.Pi / 2.0 + i * math.Pi / 5.0
        val r = if (i % 2 == 0) radius.toDouble else radius * 0.43
        points.addPoint((x + math.cos(angle) * r).toInt, (y + math.sin(angle) * r).toInt)
        i += 1
      }
      g.setColor(color)
      g.fillPolygon(points)
    }

    private def drawAlien(g: Graphics2D, x: Double, y: Double, size: Double, boss: Boolean, frame: Int): Unit = {
      val bob = math.sin(frame * 0.08 + x * 0.01) * 3.0
      val r = size
      val bodyColor = if (boss) new Color(171, 94, 255) else new Color(91, 226, 176)
      g.setColor(new Color(0, 0, 0, 65))
      g.fill(new Ellipse2D.Double(x - r * 0.8, y + r * 0.7, r * 1.6, r * 0.38))
      g.setColor(bodyColor)
      g.fill(new Ellipse2D.Double(x - r, y - r + bob, r * 2.0, r * 1.8))
      g.setColor(new Color(215, 255, 244))
      g.fill(new Ellipse2D.Double(x - r * 0.66, y - r * 0.48 + bob, r * 0.52, r * 0.58))
      g.fill(new Ellipse2D.Double(x + r * 0.14, y - r * 0.48 + bob, r * 0.52, r * 0.58))
      g.setColor(new Color(10, 38, 57))
      g.fill(new Ellipse2D.Double(x - r * 0.49, y - r * 0.27 + bob, r * 0.19, r * 0.30))
      g.fill(new Ellipse2D.Double(x + r * 0.30, y - r * 0.27 + bob, r * 0.19, r * 0.30))
      g.setStroke(new BasicStroke(2.0f))
      g.setColor(new Color(228, 255, 245))
      g.drawArc((x - r * 0.24).toInt, (y + r * 0.10 + bob).toInt, (r * 0.48).toInt, (r * 0.30).toInt, 190, 160)
      g.setColor(bodyColor.darker())
      g.drawLine((x - r * 0.32).toInt, (y - r * 0.87 + bob).toInt, (x - r * 0.55).toInt, (y - r * 1.35 + bob).toInt)
      g.drawLine((x + r * 0.32).toInt, (y - r * 0.87 + bob).toInt, (x + r * 0.55).toInt, (y - r * 1.35 + bob).toInt)
      g.setColor(Gold)
      g.fill(new Ellipse2D.Double(x - r * 0.63, y - r * 1.46 + bob, r * 0.18, r * 0.18))
      g.fill(new Ellipse2D.Double(x + r * 0.46, y - r * 1.46 + bob, r * 0.18, r * 0.18))
    }

    private def drawPlayer(g: Graphics2D): Unit = {
      val color = suitColor(selectedSuit)
      if (shieldTicks > 0) {
        g.setColor(new Color(83, 235, 255, 65))
        g.fill(new Ellipse2D.Double(playerX - 34, playerY - 36, 68, 68))
        g.setColor(new Color(118, 248, 255, 180))
        g.setStroke(new BasicStroke(2.0f))
        g.draw(new Ellipse2D.Double(playerX - 34, playerY - 36, 68, 68))
      }
      if (invulnerableTicks > 0 && tick % 8 < 4) return
      g.setColor(new Color(0, 0, 0, 75))
      g.fill(new Ellipse2D.Double(playerX - 20, playerY + 17, 40, 12))
      g.setColor(color)
      g.fill(new RoundRectangle2D.Double(playerX - 15, playerY - 22, 30, 43, 12, 12))
      g.setColor(new Color(236, 252, 255))
      g.fill(new Ellipse2D.Double(playerX - 13, playerY - 36, 26, 26))
      g.setColor(new Color(7, 43, 70))
      g.fill(new RoundRectangle2D.Double(playerX - 9, playerY - 29, 18, 10, 7, 7))
      g.setColor(Gold)
      g.fill(new Ellipse2D.Double(playerX - 5, playerY - 8, 10, 10))
    }

    private def drawCrystal(g: Graphics2D, x: Double, y: Double, phase: Double): Unit = {
      val bob = math.sin(phase) * 4.0
      val p = new Polygon()
      p.addPoint(x.toInt, (y - 12 + bob).toInt)
      p.addPoint((x + 9).toInt, (y + bob).toInt)
      p.addPoint(x.toInt, (y + 12 + bob).toInt)
      p.addPoint((x - 9).toInt, (y + bob).toInt)
      g.setColor(new Color(105, 250, 245, 95))
      g.fill(new Ellipse2D.Double(x - 17, y - 17 + bob, 34, 34))
      g.setColor(Gold)
      g.fillPolygon(p)
      g.setColor(White)
      g.drawLine(x.toInt, (y - 8 + bob).toInt, x.toInt, (y + 7 + bob).toInt)
    }

    private def drawBackground(g: Graphics2D): Unit = {
      val w = getWidth
      val h = getHeight
      g.setPaint(new GradientPaint(0, 0, BackgroundTop, 0, h, BackgroundBottom))
      g.fillRect(0, 0, w, h)
      var i = 0
      while (i < 44) {
        val sx = (i * 197 + 47) % math.max(1, w)
        val sy = (i * 113 + 29) % 330
        val alpha = 80 + (i * 17) % 150
        g.setColor(new Color(210, 245, 255, alpha))
        g.fill(new Ellipse2D.Double(sx, sy, 2 + (i % 3), 2 + (i % 3)))
        i += 1
      }
      g.setColor(new Color(84, 223, 229, 35))
      g.fill(new Ellipse2D.Double(95, 75, 220, 110))
      g.fill(new Ellipse2D.Double(690, 92, 180, 85))
      val island = new Polygon()
      island.addPoint(0, 440)
      island.addPoint(140, 390)
      island.addPoint(275, 425)
      island.addPoint(430, 365)
      island.addPoint(585, 423)
      island.addPoint(760, 370)
      island.addPoint(1000, 430)
      island.addPoint(1000, h)
      island.addPoint(0, h)
      g.setColor(new Color(18, 83, 92))
      g.fillPolygon(island)
      g.setColor(new Color(28, 134, 131))
      g.fillRect(0, 472, w, h - 472)
      g.setColor(new Color(75, 218, 213, 95))
      var wave = 0
      while (wave < 5) {
        val y = 492 + wave * 36 + (math.sin((tick + wave * 30) * 0.025) * 8).toInt
        g.setStroke(new BasicStroke(2.0f))
        g.drawArc((wave * 180 - 50), y, 250, 38, 0, 180)
        g.drawArc((wave * 180 + 70), y + 8, 250, 30, 0, 180)
        wave += 1
      }
    }

    private def drawTidalSurge(g: Graphics2D): Unit = {
      if (waveActive) {
        val y = waveY.toInt
        val waveShape = new Polygon()
        waveShape.addPoint(0, y)
        var x = 0
        while (x <= getWidth) {
          val yy = y + (math.sin((x + tick * 4) * 0.025) * 18.0).toInt
          waveShape.addPoint(x, yy)
          x += 25
        }
        waveShape.addPoint(getWidth, getHeight)
        waveShape.addPoint(0, getHeight)
        g.setColor(new Color(55, 205, 235, 105))
        g.fillPolygon(waveShape)
        g.setColor(new Color(149, 247, 255, 220))
        g.setStroke(new BasicStroke(4.0f))
        g.drawPolyline(waveShape.xpoints, waveShape.ypoints, waveShape.npoints - 2)
      }
    }

    private def drawMenu(g: Graphics2D): Unit = {
      drawBackground(g)
      drawAlien(g, 780, 300, 52, true, tick)
      drawAlien(g, 685, 365, 25, false, tick + 30)
      drawAlien(g, 885, 380, 23, false, tick + 60)
      drawText(g, "ALIEN", 92, 154, 56, White, true)
      drawText(g, "TIDAL SURGE", 92, 214, 54, Aqua, true)
      drawText(g, "EVENT 7  |  THE PLANET OF RISING TIDES", 98, 250, 17, Gold, true)
      drawText(g, "Pilot your aqua suit. Dodge the surge. Restore the crystal core.", 98, 286, 16, White)
      drawPanel(g, 72, 335, 440, 260)
      addButton(g, 105, 366, 185, 52, "START MISSION") { screen = "levels" }
      addButton(g, 305, 366, 175, 52, "TIDE SHOP") { screen = "shop" }
      addButton(g, 105, 434, 185, 52, "HOW TO PLAY") { screen = "help" }
      addButton(g, 305, 434, 175, 52, "PLAYER PROFILE") { screen = "profile" }
      drawText(g, "Pilot: " + playerName, 105, 535, 16, White, true)
      drawText(g, "Tide Crystals: " + surgeCoins, 105, 562, 16, Gold, true)
      drawText(g, "Best Score: " + bestScore, 305, 562, 16, Aqua, true)
      drawText(g, "B = Back    ENTER = Start", 105, 640, 14, new Color(195, 225, 240))
    }

    private def drawLevels(g: Graphics2D): Unit = {
      drawBackground(g)
      drawPanel(g, 90, 55, 820, 585)
      drawText(g, "SELECT YOUR TIDE MISSION", 135, 103, 31, White, true)
      drawText(g, "Clear missions to unlock the next island.", 138, 133, 15, Aqua)
      var i = 1
      while (i <= 10) {
        val col = (i - 1) % 5
        val row = (i - 1) / 5
        val x = 145 + col * 145
        val y = 185 + row * 145
        val unlocked = i <= highestLevel
        val selected = i == selectedLevel
        g.setColor(if (selected) new Color(255, 205, 101, 220) else if (unlocked) new Color(20, 126, 142, 225) else new Color(55, 72, 92, 210))
        g.fill(new RoundRectangle2D.Double(x, y, 112, 92, 18, 18))
        g.setColor(if (unlocked) Aqua else new Color(130, 150, 165))
        g.setStroke(new BasicStroke(if (selected) 4.0f else 2.0f))
        g.draw(new RoundRectangle2D.Double(x, y, 112, 92, 18, 18))
        drawText(g, "LEVEL " + i, x + 22, y + 37, 17, White, true)
        drawText(g, if (unlocked) "UNLOCKED" else "LOCKED", x + 18, y + 64, 12, if (unlocked) Gold else new Color(175, 186, 197))
        i += 1
      }
      addButton(g, 355, 505, 135, 48, "PLAY LEVEL") { startLevel(selectedLevel) }
      addButton(g, 510, 505, 135, 48, "BACK  (B)") { screen = "menu" }
      drawText(g, "Click a level tile, then press PLAY LEVEL or ENTER.", 270, 590, 14, White)
    }

    private def drawShop(g: Graphics2D): Unit = {
      drawBackground(g)
      drawPanel(g, 110, 75, 780, 535)
      drawText(g, "TIDE SHOP", 155, 125, 34, White, true)
      drawText(g, "Crystals: " + surgeCoins, 690, 125, 19, Gold, true)
      drawText(g, "Aqua Suit upgrade", 165, 220, 22, Aqua, true)
      drawText(g, "Adds one extra starting life to each mission.", 165, 252, 15, White)
      drawText(g, "Cost: 80 Tide Crystals", 165, 282, 16, Gold, true)
      addButton(g, 650, 220, 185, 54, if (suitOwned) "OWNED" else "BUY UPGRADE", !suitOwned && surgeCoins >= 80) {
        if (!suitOwned && surgeCoins >= 80) {
          surgeCoins -= 80
          suitOwned = true
          saveProgress()
          setMessage("Aqua Suit upgrade unlocked!")
        }
      }
      drawText(g, "Suit colors", 165, 370, 22, Aqua, true)
      drawText(g, "Tide Runner is free. Solar Explorer and Nebula Guard are cosmetic.", 165, 398, 14, White)
      addButton(g, 165, 430, 180, 48, if (selectedSuit == 0) "TIDE RUNNER: ACTIVE" else "TIDE RUNNER") { selectedSuit = 0; saveProgress() }
      addButton(g, 365, 430, 180, 48, if (selectedSuit == 1) "SOLAR: ACTIVE" else "SOLAR EXPLORER", surgeCoins >= 40 || selectedSuit == 1) {
        if (selectedSuit == 1 || surgeCoins >= 40) { if (selectedSuit != 1) surgeCoins -= 40; selectedSuit = 1; saveProgress() }
      }
      addButton(g, 565, 430, 180, 48, if (selectedSuit == 2) "NEBULA: ACTIVE" else "NEBULA GUARD", surgeCoins >= 60 || selectedSuit == 2) {
        if (selectedSuit == 2 || surgeCoins >= 60) { if (selectedSuit != 2) surgeCoins -= 60; selectedSuit = 2; saveProgress() }
      }
      addButton(g, 410, 535, 160, 48, "BACK  (B)") { screen = "menu" }
    }

    private def drawHelp(g: Graphics2D): Unit = {
      drawBackground(g)
      drawPanel(g, 115, 70, 770, 545)
      drawText(g, "HOW TO PLAY", 160, 120, 34, White, true)
      drawText(g, "W A S D or Arrow Keys  -  Move your pilot", 170, 195, 18, White)
      drawText(g, "SPACE  -  Fire a harmless energy pulse at alien drones", 170, 238, 18, Aqua)
      drawText(g, "Q  -  Activate your shield (costs 15 crystals)", 170, 281, 18, White)
      drawText(g, "E  -  Activate crystal magnet for a short time", 170, 324, 18, White)
      drawText(g, "P  -  Pause / Resume      R  -  Restart mission", 170, 367, 18, White)
      drawText(g, "B  -  Return to the previous screen      ENTER  -  Continue", 170, 410, 18, White)
      drawText(g, "Touching an alien drone or a tidal surge costs one life.", 170, 475, 16, Gold, true)
      drawText(g, "Collect crystals and clear the drone wave to unlock the next level.", 170, 507, 15, White)
      addButton(g, 405, 550, 170, 46, "BACK  (B)") { screen = "menu" }
    }

    private def drawProfile(g: Graphics2D): Unit = {
      drawBackground(g)
      drawPanel(g, 170, 100, 660, 480)
      drawText(g, "PILOT PROFILE", 225, 155, 34, White, true)
      drawText(g, "Player: " + playerName, 235, 235, 22, Aqua, true)
      drawText(g, "Highest unlocked level: " + highestLevel + " / 10", 235, 285, 18, White)
      drawText(g, "Tide Crystals: " + surgeCoins, 235, 330, 18, Gold, true)
      drawText(g, "Best Score: " + bestScore, 235, 375, 18, White)
      drawText(g, "Active suit: " + suitName(selectedSuit), 235, 420, 18, Aqua)
      drawText(g, "Aqua Suit upgrade: " + (if (suitOwned) "OWNED" else "NOT OWNED"), 235, 465, 17, White)
      addButton(g, 415, 515, 170, 48, "BACK  (B)") { screen = "menu" }
    }

    private def drawGame(g: Graphics2D): Unit = {
      drawBackground(g)
      drawTidalSurge(g)
      var i = 0
      while (i < crystals.length) {
        val c = crystals(i)
        drawCrystal(g, c.x, c.y, c.phase)
        i += 1
      }
      i = 0
      while (i < aliens.length) {
        val a = aliens(i)
        drawAlien(g, a.x, a.y, a.radius, a.boss, tick + a.phase.toInt)
        if (a.boss) {
          g.setColor(new Color(8, 27, 49, 210))
          g.fillRoundRect((a.x - 45).toInt, (a.y - 62).toInt, 90, 9, 8, 8)
          g.setColor(Coral)
          g.fillRoundRect((a.x - 45).toInt, (a.y - 62).toInt, (90.0 * a.hp / (3.0 + level / 3.0)).toInt.max(3), 9, 8, 8)
        }
        i += 1
      }
      i = 0
      while (i < pulses.length) {
        val p = pulses(i)
        g.setColor(new Color(75, 243, 255, 90))
        g.fill(new Ellipse2D.Double(p.x - 13, p.y - 13, 26, 26))
        g.setColor(Aqua)
        g.fill(new Ellipse2D.Double(p.x - 6, p.y - 6, 12, 12))
        i += 1
      }
      drawPlayer(g)
      drawPanel(g, 18, 16, 390, 72)
      drawText(g, "LEVEL " + level + " / 10", 35, 45, 18, White, true)
      drawText(g, "DRONES: " + defeated + " / " + enemyGoal, 175, 45, 16, Aqua, true)
      drawText(g, "LIVES: " + lives, 35, 72, 15, Coral, true)
      drawText(g, "CRYSTALS: " + surgeCoins, 175, 72, 15, Gold, true)
      drawPanel(g, 780, 16, 200, 54)
      drawText(g, "SCORE: " + score, 805, 49, 17, White, true)
      if (messageTicks > 0) {
        drawPanel(g, 250, 95, 500, 42)
        drawText(g, message, 270, 122, 14, Gold, true)
      }
      if (paused) {
        drawPanel(g, 320, 260, 360, 150)
        drawText(g, "MISSION PAUSED", 382, 315, 27, White, true)
        drawText(g, "Press P to resume", 410, 355, 16, Aqua)
      }
    }

    private def drawComplete(g: Graphics2D): Unit = {
      drawBackground(g)
      drawPanel(g, 190, 145, 620, 410)
      drawStar(g, 500, 230, 45, Gold)
      drawText(g, "TIDE MISSION CLEAR!", 315, 310, 34, White, true)
      drawText(g, "You restored this island's crystal core.", 320, 350, 17, Aqua)
      drawText(g, "Score: " + score + "     Crystals earned: " + (12 + level * 3), 320, 392, 17, Gold, true)
      if (level < 10) addButton(g, 330, 440, 155, 52, "NEXT LEVEL") { startLevel(level + 1) }
      else addButton(g, 330, 440, 155, 52, "PLAY AGAIN") { startLevel(1) }
      addButton(g, 500, 440, 155, 52, "LEVELS") { screen = "levels" }
      drawText(g, "ENTER = Next Level     B = Level Select", 342, 525, 14, White)
    }

    private def drawGameOver(g: Graphics2D): Unit = {
      drawBackground(g)
      drawPanel(g, 205, 165, 590, 360)
      drawText(g, "THE TIDE WAS TOO STRONG", 272, 245, 29, Coral, true)
      drawText(g, "Your pilot can try this mission again.", 310, 290, 17, White)
      drawText(g, "Final score: " + score, 385, 335, 19, Gold, true)
      addButton(g, 330, 395, 155, 52, "RETRY  (R)") { startLevel(level) }
      addButton(g, 500, 395, 155, 52, "LEVELS") { screen = "levels" }
    }

    private def startLevel(value: Int): Unit = {
      level = math.max(1, math.min(10, value))
      selectedLevel = level
      lives = if (suitOwned) 4 else 3
      if (level > 1 && suitOwned) lives = 4
      shieldTicks = 0
      invulnerableTicks = 0
      magnetTicks = 0
      attackCooldown = 0
      playerX = 500.0
      playerY = 500.0
      defeated = 0
      enemyGoal = 3 + level
      score = 0
      paused = false
      tick = 0
      waveY = WindowHeight + 80.0
      waveActive = false
      waveCooldown = math.max(210, 380 - level * 14)
      waveAlreadyHit = false
      aliens.clear()
      pulses.clear()
      crystals.clear()
      var i = 0
      while (i < enemyGoal) {
        val x = 90 + random.nextInt(820)
        val y = 150 + random.nextInt(300)
        val boss = (level % 5 == 0 && i == enemyGoal - 1)
        val hp = if (boss) 3 + level / 3 else 1
        aliens += new Alien(x.toDouble, y.toDouble, boss, hp, random.nextInt(100).toDouble)
        i += 1
      }
      i = 0
      while (i < 4 + level) {
        crystals += new Crystal((80 + random.nextInt(840)).toDouble, (150 + random.nextInt(430)).toDouble)
        i += 1
      }
      screen = "game"
      setMessage(if (level % 5 == 0) "Boss drone detected! Keep moving!" else "Mission started. Collect crystals and tag the drones!")
      requestFocusInWindow()
    }

    private def firePulse(): Unit = {
      if (attackCooldown > 0 || paused || screen != "game") return
      var target: Alien = null
      var bestDistance = Double.MaxValue
      var i = 0
      while (i < aliens.length) {
        val a = aliens(i)
        val dx = a.x - playerX
        val dy = a.y - playerY
        val d = dx * dx + dy * dy
        if (d < bestDistance) { bestDistance = d; target = a }
        i += 1
      }
      var vx = 0.0
      var vy = -8.0
      if (target != null) {
        val dx = target.x - playerX
        val dy = target.y - playerY
        val distance = math.max(1.0, math.sqrt(dx * dx + dy * dy))
        vx = dx / distance * 8.0
        vy = dy / distance * 8.0
      }
      pulses += new Pulse(playerX, playerY - 15, vx, vy)
      attackCooldown = 18
    }

    private def updateGame(): Unit = {
      tick += 1
      if (messageTicks > 0) messageTicks -= 1
      if (screen != "game" || paused) return
      val speed = if (shieldTicks > 0) 4.2 else 3.7
      if (up) playerY -= speed
      if (down) playerY += speed
      if (left) playerX -= speed
      if (right) playerX += speed
      playerX = math.max(28.0, math.min(getWidth - 28.0, playerX))
      playerY = math.max(112.0, math.min(getHeight - 30.0, playerY))
      if (attackCooldown > 0) attackCooldown -= 1
      if (shieldTicks > 0) shieldTicks -= 1
      if (invulnerableTicks > 0) invulnerableTicks -= 1
      if (magnetTicks > 0) magnetTicks -= 1

      var i = 0
      while (i < aliens.length) {
        val a = aliens(i)
        if (a.alive) {
          val dx = playerX - a.x
          val dy = playerY - a.y
          val distance = math.max(1.0, math.sqrt(dx * dx + dy * dy))
          val enemySpeed = (if (a.boss) 1.05 else 0.65) + level * 0.075
          a.x += dx / distance * enemySpeed
          a.y += dy / distance * enemySpeed
          if (distance < a.radius + 19 && invulnerableTicks <= 0) {
            if (shieldTicks > 0) { shieldTicks = math.max(0, shieldTicks - 75); setMessage("Shield absorbed the drone bump!") }
            else { lives -= 1; invulnerableTicks = 95; setMessage("Careful! You lost one life.") }
            a.x -= dx / distance * 45
            a.y -= dy / distance * 45
            if (lives <= 0) { screen = "gameover"; saveProgress() }
          }
        }
        i += 1
      }

      var pi = pulses.length - 1
      while (pi >= 0) {
        val p = pulses(pi)
        p.x += p.vx
        p.y += p.vy
        p.life -= 1
        var removed = p.life <= 0 || p.x < 0 || p.x > getWidth || p.y < 0 || p.y > getHeight
        var ei = 0
        while (ei < aliens.length && !removed) {
          val a = aliens(ei)
          val dx = a.x - p.x
          val dy = a.y - p.y
          if (a.alive && dx * dx + dy * dy < (a.radius + 9) * (a.radius + 9)) {
            a.hp -= 1
            removed = true
            if (a.hp <= 0) {
              a.alive = false
              defeated += 1
             
              crystals += new Crystal(a.x, a.y)
              setMessage(if (a.boss) "Boss drone tagged! Crystal core recovered!" else "Drone tagged! Collect its crystal!")
            }
          }
          ei += 1
        }
        if (removed) pulses.remove(pi)
        pi -= 1
      }

      i = 0
      while (i < crystals.length) {
        val c = crystals(i)
        c.phase += 0.12
        val dx = playerX - c.x
        val dy = playerY - c.y
        val distance = math.sqrt(dx * dx + dy * dy)
        if (magnetTicks > 0 && distance > 1.0) { c.x += dx / distance * 2.7; c.y += dy / distance * 2.7 }
        if (distance < 25) {
          surgeCoins += 1
          score += 10
          crystals.remove(i)
          i -= 1
          if (surgeCoins % 5 == 0) saveProgress()
        }
        i += 1
      }

      if (!waveActive) {
        waveCooldown -= 1
        if (waveCooldown <= 0) {
          waveActive = true
          waveAlreadyHit = false
          waveY = getHeight + 35.0
          setMessage("TIDAL SURGE! Move to higher ground!")
        }
      } else {
        waveY -= 2.8 + level * 0.18
        if (!waveAlreadyHit && math.abs(playerY - waveY) < 30.0) {
          waveAlreadyHit = true
          if (shieldTicks > 0) { shieldTicks = math.max(0, shieldTicks - 90); setMessage("Your shield blocked the tidal surge!") }
          else if (invulnerableTicks <= 0) {
            lives -= 1
            invulnerableTicks = 110
            setMessage("The surge hit your pilot! Keep moving!")
            if (lives <= 0) screen = "gameover"
          }
        }
        if (waveY < -30) {
          waveActive = false
          waveCooldown = math.max(190, 380 - level * 14)
        }
      }

      if (defeated >= enemyGoal && screen == "game") {
        screen = "complete"
        val reward = 12 + level * 3
        surgeCoins += reward
        highestLevel = math.max(highestLevel, math.min(10, level + 1))
        bestScore = math.max(bestScore, score)
        saveProgress()
        setMessage("Mission clear! You earned " + reward + " Tide Crystals.")
      }
    }

    override def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val g = graphics.create().asInstanceOf[Graphics2D]
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
      buttons.clear()
      screen match {
        case "menu" => drawMenu(g)
        case "levels" => drawLevels(g)
        case "shop" => drawShop(g)
        case "help" => drawHelp(g)
        case "profile" => drawProfile(g)
        case "game" => drawGame(g)
        case "complete" => drawComplete(g)
        case "gameover" => drawGameOver(g)
        case _ => drawMenu(g)
      }
      g.dispose()
    }

    addMouseListener(new MouseAdapter {
      override def mousePressed(event: MouseEvent): Unit = {
        if (screen == "game" && event.getButton == MouseEvent.BUTTON3) {
          firePulse()
          return
        }
        if (screen == "levels") {
          var levelIndex = 1
          while (levelIndex <= 10) {
            val col = (levelIndex - 1) % 5
            val row = (levelIndex - 1) / 5
            val rect = new Rectangle(145 + col * 145, 185 + row * 145, 112, 92)
            if (rect.contains(event.getPoint) && levelIndex <= highestLevel) selectedLevel = levelIndex
            levelIndex += 1
          }
        }
        var i = buttons.length - 1
        while (i >= 0) {
          val b = buttons(i)
          if (b.rect.contains(event.getPoint) && b.enabled) {
            b.action()
            repaint()
            requestFocusInWindow()
            return
          }
          i -= 1
        }
        if (screen == "game" && !paused && event.getButton == MouseEvent.BUTTON1) {
          val mx = event.getX.toDouble
          val my = event.getY.toDouble
          val dx = mx - playerX
          val dy = my - playerY
          val distance = math.max(1.0, math.sqrt(dx * dx + dy * dy))
          pulses += new Pulse(playerX, playerY - 15, dx / distance * 8.0, dy / distance * 8.0)
          attackCooldown = 18
        }
      }
    })

    addKeyListener(new KeyAdapter {
      override def keyPressed(event: KeyEvent): Unit = {
        val code = event.getKeyCode
        if (code == KeyEvent.VK_W || code == KeyEvent.VK_UP) up = true
        if (code == KeyEvent.VK_S || code == KeyEvent.VK_DOWN) down = true
        if (code == KeyEvent.VK_A || code == KeyEvent.VK_LEFT) left = true
        if (code == KeyEvent.VK_D || code == KeyEvent.VK_RIGHT) right = true
        if (code == KeyEvent.VK_P && screen == "game") paused = !paused
        if (code == KeyEvent.VK_B) {
          if (screen == "game" || screen == "complete" || screen == "gameover" || screen == "shop" || screen == "help" || screen == "profile") screen = "menu"
          else if (screen == "levels") screen = "menu"
        }
        if (code == KeyEvent.VK_ENTER) {
          if (screen == "menu") screen = "levels"
          else if (screen == "levels") startLevel(selectedLevel)
          else if (screen == "complete" && level < 10) startLevel(level + 1)
          else if (screen == "complete" && level == 10) screen = "levels"
        }
        if (code == KeyEvent.VK_SPACE && screen == "game" && !paused) firePulse()
        if (code == KeyEvent.VK_Q && screen == "game" && !paused) {
          if (surgeCoins >= 15 && shieldTicks <= 0) {
            surgeCoins -= 15
            shieldTicks = 190
            saveProgress()
            setMessage("Aqua shield activated!")
          } else if (shieldTicks > 0) setMessage("Shield is already active.")
          else setMessage("You need 15 Tide Crystals for a shield.")
        }
        if (code == KeyEvent.VK_E && screen == "game" && !paused) {
          if (surgeCoins >= 10 && magnetTicks <= 0) {
            surgeCoins -= 10
            magnetTicks = 240
            saveProgress()
            setMessage("Crystal magnet activated!")
          } else if (magnetTicks > 0) setMessage("Crystal magnet is already active.")
          else setMessage("You need 10 Tide Crystals for a magnet.")
        }
        if (code == KeyEvent.VK_R && screen == "gameover") startLevel(level)
        repaint()
      }

      override def keyReleased(event: KeyEvent): Unit = {
        val code = event.getKeyCode
        if (code == KeyEvent.VK_W || code == KeyEvent.VK_UP) up = false
        if (code == KeyEvent.VK_S || code == KeyEvent.VK_DOWN) down = false
        if (code == KeyEvent.VK_A || code == KeyEvent.VK_LEFT) left = false
        if (code == KeyEvent.VK_D || code == KeyEvent.VK_RIGHT) right = false
      }
    })

    addFocusListener(new java.awt.event.FocusAdapter {
      override def focusLost(event: java.awt.event.FocusEvent): Unit = {
        up = false
        down = false
        left = false
        right = false
      }
    })
  }

  def main(args: Array[String]): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = {
        val entered = JOptionPane.showInputDialog(null, "Enter your Alien Tidal Surge pilot name:",
          "Alien Tidal Surge Login", JOptionPane.QUESTION_MESSAGE)
        val name = if (entered == null || entered.trim.isEmpty) "Pilot" else entered.trim.take(18)
        val panel = new GamePanel(name)
        val frame = new JFrame(AppTitle)
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
        frame.setContentPane(panel)
        frame.pack()
        frame.setMinimumSize(new Dimension(850, 620))
        frame.setLocationRelativeTo(null)
        frame.setVisible(true)
        panel.startLoop()
        panel.requestFocusInWindow()
      }
    })
  }
}

AlienTidalSurge.main(Array[String]())