Code Sketch


Bixuu spring season event 6
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.io.{BufferedReader, InputStreamReader, PrintWriter}
import java.net.{InetAddress, InetSocketAddress, ServerSocket, Socket}
import java.security.MessageDigest
import java.util.Locale
import java.util.prefs.Preferences
import javax.swing.{JFrame, JOptionPane, JPanel, JPasswordField, SwingUtilities, Timer, WindowConstants}
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.util.Try

/**
 * SPRING SEASON EVENT 6 - FIGHTING BLOOM
 * Original, stylized arcade battle game for Scala / Java Swing.
 * Uses Java Swing only; no external game-engine dependency.
 *
 * This is a 2D Swing game with perspective-style art, not a full 3D engine.
 * Profiles and game progress are saved locally on this computer.
 * LAN multiplayer is a small one-peer demo over the same Wi-Fi network.
 * It is not an internet account service or a secure production game server.
 */
object BixuuSpringSeasonEvent6 {
  private val AppTitle = "SPRING SEASON EVENT 6 - FIGHTING BLOOM"
  private val AppRoot = Preferences.userRoot().node("BixuuGames/FightingBloom")
  private val Profiles = AppRoot.node("profiles")
  // Canonical administrator ID requested for Event 6.
  private val AdminId = "BixubuxShah25"
  // SHA-256 digests for supported admin ID spellings.
  // These protect a LOCAL DEMO profile only; do not use this as production authentication.
  private val AdminPasswordHashes = Map(
    "bixubuxshah25" -> "cca2f82edaacfa7bef39e4a7128804e6437605037d4886fc8d04e8184e701d6d",
    "bixubuxshahare25" -> "266e7000a41697c2908a9b920baaed085cf69c4a27433b2075c9d66df5459d83",
    "bixubux" -> "b621541d5dc8439962c0ea4ea2aec5dd332add4af2d5d0e5855e29948a70f182"
  )

  private val Bg = new Color(8, 22, 32)
  private val PanelColor = new Color(13, 39, 49, 235)
  private val Mint = new Color(126, 255, 197)
  private val Gold = new Color(255, 216, 105)
  private val Pink = new Color(255, 125, 174)
  private val White = new Color(240, 250, 248)
  private val Muted = new Color(163, 193, 190)

  private class PlayerData(val id: String, var passwordHash: String, var coins: Int,
                           var diamonds: Int, var level: Int, var xp: Int,
                           var avatar: Int, var bundle: String, var powerStyle: String,
                           var tags: Int, var wins: Int) {
    def xpTarget: Int = 100 + (level - 1) * 30
  }
  private case class UiButton(rect: Rectangle, label: String, action: () => Unit, enabled: Boolean = true)
  private class Bot(var x: Double, var y: Double, var tagsLeft: Int, val maxTags: Int,
                    val name: String, val trainingDummy: Boolean) {
    var alive = true
  }
  private class Loot(var x: Double, var y: Double, val kind: String, val amount: Int) {
    var taken = false
  }
  // Magical, non-realistic Bloom energy projectile used by the arcade light-tag game.
  private class BloomBolt(var x: Double, var y: Double, val vx: Double, val vy: Double, var life: Double) {
    var active = true
  }
  private case class RemotePlayer(id: String, x: Double, y: Double, avatar: Int, energy: Int)

  private val avatarNames = Array("Bloom Runner", "Moss Mage", "Crystal Guard", "Sun Scout")
  private val bundles = Array("Starter Petals", "Leaf Ranger", "Crystal Bloom", "Solar Champion")
  private val bundleCoins = Array(0, 150, 260, 420)
  private val bundleDiamonds = Array(0, 0, 25, 50)
  private val powerCoins = Array(0, 100, 220, 350)
  private val powerDiamonds = Array(0, 0, 0, 20)
  private val powerNames = Array("Bloom Dash", "Petal Shield", "Healing Bloom", "Nature Nova")
  private val costumeNames = Array("Mint Ranger", "Cherry Blossom", "Crystal Bloom", "Golden Guardian")
  private val blasterNames = Array("Petal Pulse", "Crystal Beam", "Sun Orb")

  private var current: PlayerData = null
  private var adminMode = false
  private var screen = "login"
  private var statusMessage = "Welcome to Fighting Bloom!"
  private var selectedAvatar = 0
  private var selectedBundle = 0
  private var selectedPowerStyle = 0
  private var selectedCostume = 0
  private var selectedBlaster = 0
  private var gameMode = "BR"
  private var gameRunning = false
  private var matchFinished = false
  private var matchWon = false
  private var dropPhase = false
  private var aircraftX = -80.0
  private var aircraftY = 125.0
  private var landingX = 500.0
  private var landingY = 340.0
  private var landingFlash = 0.0
  private var playerX = 500.0
  private var playerY = 385.0
  private var aimX = 650.0
  private var aimY = 350.0
  private var playerEnergy = 100
  private var playerMaxEnergy = 100
  private var matchSeconds = 0.0
  private var zoneRadius = 340.0
  private var tagCooldown = 0.0
  private var shotCooldown = 0.0
  private var powerCooldown = 0.0
  private var zoneDrainCooldown = 0.0
  private var airdropMade = false
  private var rewardGiven = false
  private var matchTags = 0
  private var matchCoins = 0
  @volatile private var remotePlayer: RemotePlayer = null
  @volatile private var lanChannel: LanChannel = null
  @volatile private var lanServer: ServerSocket = null
  @volatile private var lanStatus = "LAN: not connected"
  private var hostAcceptThread: Thread = null
  private val bots = ArrayBuffer[Bot]()
  private val loots = ArrayBuffer[Loot]()
  private val bloomBolts = ArrayBuffer[BloomBolt]()
  private val heldKeys = mutable.Set[Int]()
  private val buttons = ArrayBuffer[UiButton]()
  private var frame: JFrame = null
  private var canvas: GamePanel = null
  private var lastLanSend = 0.0

  private def sha256(text: String): String = {
    val bytes = MessageDigest.getInstance("SHA-256").digest(text.getBytes("UTF-8"))
    bytes.map(b => "%02x".format(b & 0xff)).mkString
  }

  private def safeId(id: String): String = id.trim.toLowerCase(Locale.ROOT)
  private def validId(id: String): Boolean = id != null && id.matches("[A-Za-z0-9_-]{3,18}")
  private def profileNode(id: String) = Profiles.node(safeId(id))

  private def savePlayer(p: PlayerData): Unit = {
    if (p != null) {
      val n = profileNode(p.id)
      n.put("exists", "1")
      n.put("id", p.id)
      n.put("passwordHash", p.passwordHash)
      n.putInt("coins", math.max(0, p.coins))
      n.putInt("diamonds", math.max(0, p.diamonds))
      n.putInt("level", math.max(1, math.min(20, p.level)))
      n.putInt("xp", math.max(0, p.xp))
      n.putInt("avatar", math.max(0, math.min(avatarNames.length - 1, p.avatar)))
      n.put("bundle", p.bundle)
      n.put("powerStyle", p.powerStyle)
      n.putInt("tags", math.max(0, p.tags))
      n.putInt("wins", math.max(0, p.wins))
      try { n.flush(); AppRoot.flush() } catch { case _: Throwable => () }
    }
  }

  private def loadPlayer(id: String): PlayerData = {
    val n = profileNode(id)
    new PlayerData(n.get("id", id), n.get("passwordHash", ""),
      math.max(0, n.getInt("coins", 500)), math.max(0, n.getInt("diamonds", 30)),
      math.max(1, math.min(20, n.getInt("level", 1))), math.max(0, n.getInt("xp", 0)),
      math.max(0, math.min(avatarNames.length - 1, n.getInt("avatar", 0))),
      n.get("bundle", bundles(0)), n.get("powerStyle", powerNames(0)),
      math.max(0, n.getInt("tags", 0)), math.max(0, n.getInt("wins", 0)))
  }

  private def showPasswordDialog(title: String): Option[String] = {
    val field = new JPasswordField(18)
    val result = JOptionPane.showConfirmDialog(frame, field, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)
    if (result == JOptionPane.OK_OPTION) Some(new String(field.getPassword)) else None
  }

  private def loginDialog(): Unit = {
    val id = JOptionPane.showInputDialog(frame, "Enter your Player ID:", "Player Login", JOptionPane.QUESTION_MESSAGE)
    if (id == null) return
    val clean = id.trim
    if (!validId(clean)) { message("Use 3-18 letters, numbers, _ or - for your ID."); return }
    showPasswordDialog("Enter password for " + clean).foreach { pass =>
      val supplied = sha256("FIGHTINGBLOOM|" + safeId(clean) + "|" + pass)
      val isAdminLogin = AdminPasswordHashes.get(safeId(clean)).contains(supplied)
      val node = profileNode(if (isAdminLogin) AdminId else clean)
      val stored = node.get("passwordHash", "")
      if (isAdminLogin || (node.get("exists", "") == "1" && stored == supplied)) {
        if (isAdminLogin && node.get("exists", "") != "1") {
          node.put("exists", "1"); node.put("id", AdminId); node.put("passwordHash", AdminPasswordHashes("bixubuxshah25"))
          node.putInt("coins", 1000); node.putInt("diamonds", 500); node.putInt("level", 1)
          node.putInt("xp", 0); node.putInt("avatar", 0); node.put("bundle", bundles(0))
          node.put("powerStyle", powerNames(0)); node.putInt("tags", 0); node.putInt("wins", 0)
          node.putInt("costume", 0); node.putInt("blaster", 0)
        }
        current = loadPlayer(if (isAdminLogin) AdminId else clean)
        adminMode = isAdminLogin
        selectedCostume = math.max(0, math.min(costumeNames.length - 1, profileNode(current.id).getInt("costume", 0)))
        selectedBlaster = math.max(0, math.min(blasterNames.length - 1, profileNode(current.id).getInt("blaster", 0)))
        AppRoot.put("lastId", current.id)
        try { AppRoot.flush() } catch { case _: Throwable => () }
        screen = "menu"
        statusMessage = "Welcome, " + current.id + "!"
        if (canvas != null) canvas.requestFocusInWindow()
      } else {
        message("Login failed. Check your ID and password, or create an account first.")
      }
    }
  }

  private def createAccountDialog(): Unit = {
    val id = JOptionPane.showInputDialog(frame, "Choose a Player ID (3-18 letters/numbers):", "Create Account", JOptionPane.QUESTION_MESSAGE)
    if (id == null) return
    val clean = id.trim
    if (!validId(clean)) { message("Use 3-18 letters, numbers, _ or -."); return }
    if (AdminPasswordHashes.contains(safeId(clean))) { message("That ID is reserved for the game administrator."); return }
    val node = profileNode(clean)
    if (node.get("exists", "") == "1") { message("That Player ID already exists on this computer."); return }
    showPasswordDialog("Create a password (at least 4 characters)").foreach { pass =>
      if (pass.length < 4) message("Password must have at least 4 characters.")
      else {
        val p = new PlayerData(clean, sha256("FIGHTINGBLOOM|" + safeId(clean) + "|" + pass),
          500, 30, 1, 0, 0, bundles(0), powerNames(0), 0, 0)
        savePlayer(p)
        current = p
        adminMode = false
        selectedCostume = 0
        selectedBlaster = 0
        profileNode(clean).putInt("costume", 0)
        profileNode(clean).putInt("blaster", 0)
        AppRoot.put("lastId", clean)
        screen = "menu"
        statusMessage = "Account created! Your profile is saved on this computer."
      }
    }
  }

  private def message(s: String): Unit = JOptionPane.showMessageDialog(frame, s, AppTitle, JOptionPane.INFORMATION_MESSAGE)

  private def logout(): Unit = {
    savePlayer(current)
    current = null
    adminMode = false
    screen = "login"
    statusMessage = "Logged out. Your local profile is saved."
    closeLan()
  }

  private def addXp(amount: Int): Unit = {
    if (current == null) return
    current.xp += amount
    while (current.level < 20 && current.xp >= current.xpTarget) {
      current.xp -= current.xpTarget
      current.level += 1
      current.coins += 75
      statusMessage = "LEVEL UP! You reached level " + current.level + " (+75 coins)."
    }
    if (current.level >= 20) current.xp = math.min(current.xp, current.xpTarget)
    savePlayer(current)
  }

  private def startMatch(mode: String): Unit = {
    if (current == null) { screen = "login"; return }
    gameMode = mode
    gameRunning = true
    matchFinished = false
    matchWon = false
    dropPhase = mode == "BR"
    aircraftX = -85.0; aircraftY = 145.0
    landingX = 500.0; landingY = 340.0; landingFlash = 0.0
    playerX = 490.0; playerY = if (dropPhase) 145.0 else 365.0
    aimX = 640.0; aimY = 350.0
    playerEnergy = 100; playerMaxEnergy = 100
    matchSeconds = 0.0; zoneRadius = 340.0
    tagCooldown = 0.0; shotCooldown = 0.0; powerCooldown = 0.0; zoneDrainCooldown = 0.0
    airdropMade = false; rewardGiven = false; matchTags = 0; matchCoins = 0
    bots.clear(); loots.clear(); bloomBolts.clear()
    val count = if (mode == "BR") 8 else if (mode == "CS") 3 else 4
    var i = 0
    while (i < count) {
      val bx = 145.0 + ((i * 137) % 700)
      val by = 180.0 + ((i * 91) % 360)
      val tagsNeeded = if (mode == "TRAINING") 1 else 2
      bots += new Bot(bx, by, tagsNeeded, tagsNeeded,
        "Bloom Bot " + (i + 1), mode == "TRAINING")
      i += 1
    }
    loots += new Loot(250, 275, "COINS", 35)
    loots += new Loot(740, 250, "STAR", 18)
    loots += new Loot(620, 510, "ENERGY", 25)
    loots += new Loot(350, 490, "COINS", 25)
    loots += new Loot(805, 430, "DIAMOND", 2)
    screen = "battle"
    statusMessage = if (dropPhase) "You are aboard the Bloom Airship. Choose a map point, then press ENTER to parachute down." else "Training ground ready. Move with arrows / W-A-D; press S to fire a Bloom energy orb."
    if (canvas != null) canvas.requestFocusInWindow()
  }

  private def finishMatch(won: Boolean): Unit = {
    if (matchFinished) return
    matchFinished = true
    gameRunning = false
    matchWon = won
    if (!rewardGiven && current != null) {
      val reward = if (won) 180 else 35
      current.coins += reward + matchCoins
      current.tags += matchTags
      if (won) current.wins += 1
      addXp((if (won) 120 else 45) + matchTags * 18)
      rewardGiven = true
      savePlayer(current)
    }
    statusMessage = if (won) "VICTORY! The Bloom Arena is yours." else "MATCH OVER. Train and return stronger."
  }

  private def usePower(): Unit = {
    if (screen != "battle" || matchFinished || dropPhase || powerCooldown > 0.0) return
    val power = if (current == null) powerNames(0) else current.powerStyle match {
      case "Starter Petals" => powerNames(0)
      case "Crystal Bloom" => powerNames(1)
      case "Solar Champion" => powerNames(3)
      case _ => powerNames(2)
    }
    power match {
      case "Bloom Dash" => playerX = clamp(playerX + (aimX - playerX) * 0.45, 35, 965); playerY = clamp(playerY + (aimY - playerY) * 0.45, 70, 615); statusMessage = "BLOOM DASH!"
      case "Petal Shield" => playerEnergy = math.min(playerMaxEnergy, playerEnergy + 20); statusMessage = "PETAL SHIELD restored your energy."
      case "Healing Bloom" => playerEnergy = math.min(playerMaxEnergy, playerEnergy + 32); statusMessage = "HEALING BLOOM restored energy."
      case _ =>
        bots.filter(_.alive).foreach(b => if (distance(playerX, playerY, b.x, b.y) < 260) tagBot(b, 1))
        statusMessage = "NATURE NOVA! Nearby Bloom Bots received a light-tag pulse."
    }
    powerCooldown = 8.0
  }

  private def tagBot(bot: Bot, tags: Int): Unit = {
    if (bot != null && bot.alive) {
      bot.tagsLeft -= tags
      if (bot.tagsLeft <= 0) {
        bot.alive = false
        matchTags += 1
        matchCoins += 12
        addXp(18)
        statusMessage = "BLOOM BOT TAGGED! +12 coins"
      } else {
        statusMessage = "One light-tag recorded. Tag again to score!"
      }
    }
  }

  private def tagBotAt(x: Double, y: Double): Unit = {
    if (screen != "battle" || matchFinished || dropPhase || tagCooldown > 0.0) return
    val candidate = bots.filter(_.alive).sortBy(b => distance(x, y, b.x, b.y)).headOption
    candidate match {
      case Some(bot) if distance(x, y, bot.x, bot.y) < 48 && distance(playerX, playerY, bot.x, bot.y) < 210 => tagBot(bot, 1)
      case _ => statusMessage = "Move closer, then click a Bloom Bot to light-tag it."
    }
    tagCooldown = 0.45
    aimX = x; aimY = y
  }

  private def shootBloomBolt(): Unit = {
    if (screen != "battle" || matchFinished || dropPhase || shotCooldown > 0.0) return
    var dx = aimX - playerX
    var dy = aimY - playerY
    var length = math.sqrt(dx * dx + dy * dy)
    if (length < 1.0) { dx = 1.0; dy = 0.0; length = 1.0 }
    val speed = 12.0
    bloomBolts += new BloomBolt(playerX, playerY - 8.0, dx / length * speed, dy / length * speed, 1.4)
    shotCooldown = 0.28
    statusMessage = "BLOOM PULSE! A magical energy orb is on its way."
  }

  private def deployFromAirship(): Unit = {
    if (screen == "battle" && dropPhase) {
      dropPhase = false
      playerX = clamp(landingX, 35, 965)
      playerY = clamp(landingY, 80, 615)
      landingFlash = 1.6
      statusMessage = "Parachute landing complete! Collect glowing loot with E."
    }
  }

  private def collectNearby(): Unit = {
    val item = loots.find(l => !l.taken && distance(playerX, playerY, l.x, l.y) < 48)
    item.foreach { l =>
      l.taken = true
      l.kind match {
        case "COINS" => matchCoins += l.amount; statusMessage = "+" + l.amount + " COINS (added after match)"
        case "DIAMOND" => current.diamonds += l.amount; statusMessage = "+" + l.amount + " DIAMONDS"
        case "ENERGY" => playerEnergy = math.min(playerMaxEnergy, playerEnergy + l.amount); statusMessage = "ENERGY RESTORED"
        case "STAR" => matchCoins += l.amount; statusMessage = "STAR SHARDS COLLECTED"
        case _ => ()
      }
      savePlayer(current)
    }
  }

  private def updateGame(): Unit = {
    if (screen != "battle" || !gameRunning || matchFinished) return
    val dt = 0.033
    matchSeconds += dt
    if (tagCooldown > 0) tagCooldown = math.max(0.0, tagCooldown - dt)
    if (shotCooldown > 0) shotCooldown = math.max(0.0, shotCooldown - dt)
    if (powerCooldown > 0) powerCooldown = math.max(0.0, powerCooldown - dt)
    if (zoneDrainCooldown > 0) zoneDrainCooldown = math.max(0.0, zoneDrainCooldown - dt)
    if (landingFlash > 0) landingFlash = math.max(0.0, landingFlash - dt)
    if (dropPhase) {
      aircraftX += 5.0
      if (aircraftX > 1080.0) aircraftX = -100.0
    } else {
      var dx = 0.0; var dy = 0.0
      // S is reserved for shooting; use the arrow keys for full movement and W/A/D as alternatives.
      if (heldKeys.contains(KeyEvent.VK_W) || heldKeys.contains(KeyEvent.VK_UP)) dy -= 1
      if (heldKeys.contains(KeyEvent.VK_DOWN)) dy += 1
      if (heldKeys.contains(KeyEvent.VK_A) || heldKeys.contains(KeyEvent.VK_LEFT)) dx -= 1
      if (heldKeys.contains(KeyEvent.VK_D) || heldKeys.contains(KeyEvent.VK_RIGHT)) dx += 1
      val mag = math.max(1.0, math.sqrt(dx * dx + dy * dy))
      if (dx != 0 || dy != 0) { playerX = clamp(playerX + dx / mag * 4.0, 32, 968); playerY = clamp(playerY + dy / mag * 4.0, 72, 618) }
      // Move magical Bloom energy orbs and register non-realistic light-tag hits.
      bloomBolts.foreach { bolt =>
        if (bolt.active) {
          bolt.x += bolt.vx
          bolt.y += bolt.vy
          bolt.life -= dt
          val hit = bots.find(b => b.alive && distance(bolt.x, bolt.y, b.x, b.y) < 27.0)
          hit match {
            case Some(bot) => tagBot(bot, 1); bolt.active = false
            case None => if (bolt.life <= 0.0 || bolt.x < 0 || bolt.x > 1000 || bolt.y < 65 || bolt.y > 665) bolt.active = false
          }
        }
      }
      val liveBolts = bloomBolts.filter(_.active)
      bloomBolts.clear()
      bloomBolts ++= liveBolts
      if (gameMode == "BR") zoneRadius = math.max(115.0, 340.0 - matchSeconds * 1.2)
      if (gameMode == "BR" && matchSeconds > 24.0 && !airdropMade) {
        loots += new Loot(500, 330, "DIAMOND", 5)
        airdropMade = true
        statusMessage = "A BLOOM DROP has arrived near the arena center!"
      }
      // Bloom Bots roam gently around the arena in a non-contact tag challenge.
      bots.filter(b => b.alive && !b.trainingDummy).zipWithIndex.foreach { pair =>
        val bot = pair._1
        val phase = matchSeconds * 0.55 + pair._2 * 1.7
        bot.x = clamp(bot.x + math.sin(phase) * 0.55, 55, 945)
        bot.y = clamp(bot.y + math.cos(phase * 0.8) * 0.45, 85, 615)
      }
      if (gameMode == "BR" && distance(playerX, playerY, 500, 340) > zoneRadius && zoneDrainCooldown <= 0.0) {
        playerEnergy -= 1
        zoneDrainCooldown = 1.0
      }
      if (gameMode == "TRAINING") playerEnergy = math.min(playerMaxEnergy, playerEnergy + 1)
      if (playerEnergy <= 0) finishMatch(false)
      if (bots.forall(b => !b.alive)) finishMatch(true)
    
      // A decorative transport glides overhead until the player chooses to land.
      if (matchSeconds > 40) matchSeconds = 0
    }
    lastLanSend += dt
    if (lastLanSend >= 0.12) { sendLanState(); lastLanSend = 0.0 }
  }

  private def clamp(v: Double, lo: Double, hi: Double): Double = math.max(lo, math.min(hi, v))
  private def distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = {
    val dx = x1 - x2; val dy = y1 - y2; math.sqrt(dx * dx + dy * dy)
  }

  private class LanChannel(sock: Socket) {
    private val reader = new BufferedReader(new InputStreamReader(sock.getInputStream, "UTF-8"))
    private val writer = new PrintWriter(sock.getOutputStream, true)
    @volatile private var remote: RemotePlayer = null
    @volatile private var closed = false
    private val readThread = new Thread(new Runnable {
      override def run(): Unit = {
        try {
          var line: String = null
          while (!closed && { line = reader.readLine(); line != null }) {
            val p = line.split("\\|", 6)
            if (p.length == 6 && p(0) == "P") {
              val x = Try(p(2).toDouble).getOrElse(500.0)
              val y = Try(p(3).toDouble).getOrElse(350.0)
              val av = Try(p(4).toInt).getOrElse(0)
              val energy = Try(p(5).toInt).getOrElse(100)
              remote = RemotePlayer(p(1).take(18), x, y, math.max(0, math.min(3, av)), math.max(0, math.min(100, energy)))
            }
          }
        } catch { case _: Throwable => () }
        finally { close() }
      }
    }, "FightingBloom-LAN-Reader")
    readThread.setDaemon(true)
    readThread.start()
    def send(line: String): Unit = synchronized { if (!closed) { writer.println(line); if (writer.checkError()) close() } }
    def latest: Option[RemotePlayer] = Option(remote)
    def close(): Unit = { if (!closed) { closed = true; try { sock.close() } catch { case _: Throwable => () } } }
  }

  private def hostLan(): Unit = {
    try {
      val input = JOptionPane.showInputDialog(frame, "Port for your Wi-Fi room:", "5055")
      if (input == null) return
      val port = Try(input.trim.toInt).getOrElse(5055)
      if (port < 1024 || port > 65535) { message("Choose a port from 1024 to 65535."); return }
      closeLan()
      val server = new ServerSocket(port)
      lanServer = server
      lanStatus = "HOSTING on " + InetAddress.getLocalHost.getHostAddress + ":" + port + " (waiting for 1 player)"
      hostAcceptThread = new Thread(new Runnable {
        override def run(): Unit = {
          try {
            val socket = server.accept()
            lanChannel = new LanChannel(socket)
            lanStatus = "LAN connected: 1 player"
          } catch { case _: Throwable => lanStatus = "LAN host closed" }
        }
      }, "FightingBloom-LAN-Host")
      hostAcceptThread.setDaemon(true); hostAcceptThread.start()
      statusMessage = lanStatus
      message("Room created! Share this IP and port with the other player:\n" + lanStatus + "\nBoth computers must be on the same Wi-Fi. Allow the app through Windows Firewall if asked.")
    } catch { case e: Throwable => message("Could not host LAN room: " + e.getMessage) }
  }

  private def joinLan(): Unit = {
    val ip = JOptionPane.showInputDialog(frame, "Enter the host computer's Wi-Fi IP address:", "192.168.1.10")
    if (ip == null || ip.trim.isEmpty) return
    val portText = JOptionPane.showInputDialog(frame, "Enter room port:", "5055")
    if (portText == null) return
    val port = Try(portText.trim.toInt).getOrElse(5055)
    try {
      closeLan()
      val socket = new Socket()
      socket.connect(new InetSocketAddress(ip.trim, port), 4000)
      lanChannel = new LanChannel(socket)
      lanStatus = "CONNECTED to " + ip.trim + ":" + port
      statusMessage = lanStatus
      message("Connected! Start a match on both computers to see each other's player marker.")
    } catch { case e: Throwable => message("Could not connect. Check the IP, port, Wi-Fi and Firewall.\n" + e.getMessage) }
  }

  private def sendLanState(): Unit = {
    if (lanChannel != null && current != null && screen == "battle") {
      val id = current.id.replace("|", "_")
      lanChannel.send("P|" + id + "|" + playerX + "|" + playerY + "|" + current.avatar + "|" + playerEnergy)
      remotePlayer = lanChannel.latest.orNull
      if (remotePlayer != null) lanStatus = "LAN player: " + remotePlayer.id
    }
  }

  private def closeLan(): Unit = {
    if (lanChannel != null) { lanChannel.close(); lanChannel = null }
    if (lanServer != null) { try { lanServer.close() } catch { case _: Throwable => () }; lanServer = null }
    remotePlayer = null
    lanStatus = "LAN: not connected"
  }

  private def adminSearch(): Unit = {
    if (!adminMode) { message("Admin access required."); return }
    val query = JOptionPane.showInputDialog(frame, "Search a Player ID:", "Admin Player Search", JOptionPane.QUESTION_MESSAGE)
    if (query == null || query.trim.isEmpty) return
    try {
      val children = Profiles.childrenNames()
      val found = children.toSeq.map(n => Profiles.node(n)).find { n =>
        n.get("id", "").equalsIgnoreCase(query.trim) || n.get("id", "").toLowerCase(Locale.ROOT).contains(query.trim.toLowerCase(Locale.ROOT))
      }
      found match {
        case Some(n) =>
          val playerId = n.get("id", "Unknown")
          val amountText = JOptionPane.showInputDialog(frame,
            "Player: " + playerId + "\nCoins: " + n.getInt("coins", 0) + "  Diamonds: " + n.getInt("diamonds", 0) +
              "\nEnter diamonds to add (0-100000):", "100")
          if (amountText != null) {
            val amount = Try(amountText.trim.toInt).getOrElse(-1)
            if (amount < 0 || amount > 100000) message("Enter a number from 0 to 100000.")
            else {
              n.putInt("diamonds", math.min(2000000000L, n.getInt("diamonds", 0).toLong + amount.toLong).toInt)
              try { n.flush() } catch { case _: Throwable => () }
              message("Added " + amount + " diamonds to " + playerId + ".")
              if (current != null && current.id.equalsIgnoreCase(playerId)) current.diamonds = n.getInt("diamonds", current.diamonds)
            }
          }
        case None => message("No matching local player ID found. Player profiles are saved on this computer only.")
      }
    } catch { case e: Throwable => message("Admin search failed: " + e.getMessage) }
  }

  private def buyBundle(index: Int): Unit = {
    if (current == null || index < 0 || index >= bundles.length) return
    if (current.bundle == bundles(index)) { message("This bundle is already equipped."); return }
    if (current.coins >= bundleCoins(index) && current.diamonds >= bundleDiamonds(index)) {
      current.coins -= bundleCoins(index); current.diamonds -= bundleDiamonds(index)
      current.bundle = bundles(index); selectedBundle = index; savePlayer(current)
      statusMessage = bundles(index) + " equipped!"
    } else message("Not enough coins or diamonds for this bundle.")
  }

  private def buyPowerStyle(index: Int): Unit = {
    if (current == null || index < 0 || index >= powerNames.length) return
    val ownedKey = "powerOwned_" + index
    val node = profileNode(current.id)
    val owned = index == 0 || node.getBoolean(ownedKey, false)
    if (owned) { current.powerStyle = powerNames(index); savePlayer(current); statusMessage = powerNames(index) + " equipped." }
    else if (current.coins >= powerCoins(index) && current.diamonds >= powerDiamonds(index)) {
      current.coins -= powerCoins(index); current.diamonds -= powerDiamonds(index)
      node.putBoolean(ownedKey, true); current.powerStyle = powerNames(index); savePlayer(current)
      statusMessage = powerNames(index) + " unlocked and equipped!"
    } else message("Not enough coins or diamonds.")
  }

  private def drawBackground(g: Graphics2D, w: Int, h: Int): Unit = {
    g.setPaint(new GradientPaint(0, 0, new Color(8, 24, 37), 0, h, new Color(14, 73, 64)))
    g.fillRect(0, 0, w, h)
    var i = 0
    while (i < 18) {
      val x = (i * 67 + 23) % w
      val y = (i * 43 + 31) % h
      g.setColor(new Color(91, 255, 183, 18))
      g.fill(new Ellipse2D.Double(x, y, 40 + (i % 4) * 14, 40 + (i % 4) * 14))
      i += 1
    }
    g.setColor(new Color(255, 255, 255, 12))
    for (x <- 0 until w by 70) g.drawLine(x, 0, x - 90, h)
  }

  private def drawTitle(g: Graphics2D, w: Int, y: Int): Unit = {
    g.setFont(new Font("SansSerif", Font.BOLD, 34)); g.setColor(Mint)
    val a = "SPRING SEASON EVENT 6"
    g.drawString(a, (w - g.getFontMetrics.stringWidth(a)) / 2, y)
    g.setFont(new Font("SansSerif", Font.BOLD, 45)); g.setColor(Gold)
    val b = "FIGHTING BLOOM"
    g.drawString(b, (w - g.getFontMetrics.stringWidth(b)) / 2, y + 48)
  }

  private def drawText(g: Graphics2D, s: 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(s, x, y)
  }

  private def drawCentered(g: Graphics2D, s: 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(s, x - g.getFontMetrics.stringWidth(s) / 2, y)
  }

  private def drawButton(g: Graphics2D, text: String, x: Int, y: Int, w: Int, h: Int,
                         action: () => Unit, enabled: Boolean = true): Unit = {
    val rect = new Rectangle(x, y, w, h)
    buttons += UiButton(rect, text, action, enabled)
    g.setColor(if (enabled) new Color(26, 104, 85, 235) else new Color(62, 76, 75, 200))
    g.fill(new RoundRectangle2D.Double(x, y, w, h, 18, 18))
    g.setStroke(new BasicStroke(2f)); g.setColor(if (enabled) Mint else Muted)
    g.draw(new RoundRectangle2D.Double(x, y, w, h, 18, 18))
    g.setFont(new Font("SansSerif", Font.BOLD, 15)); g.setColor(if (enabled) White else Muted)
    g.drawString(text, x + (w - g.getFontMetrics.stringWidth(text)) / 2, y + h / 2 + 5)
  }

  private def drawCard(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, title: String, detail: String): Unit = {
    g.setColor(PanelColor); g.fill(new RoundRectangle2D.Double(x, y, w, h, 20, 20))
    g.setColor(new Color(104, 255, 198, 130)); g.draw(new RoundRectangle2D.Double(x, y, w, h, 20, 20))
    drawText(g, title, x + 18, y + 31, 17, Gold, true)
    detail.split("\\n").zipWithIndex.foreach { pair => drawText(g, pair._1, x + 18, y + 59 + pair._2 * 17, 12, White) }
  }

  private def drawProfileBar(g: Graphics2D): Unit = {
    if (current == null) return
    g.setColor(new Color(4, 17, 26, 220)); g.fillRoundRect(18, 16, 964, 44, 16, 16)
    drawText(g, current.id + "   ?   LV " + current.level, 32, 44, 15, White, true)
    drawText(g, "COINS " + current.coins, 400, 44, 14, Gold, true)
    drawText(g, "DIAMONDS " + current.diamonds, 560, 44, 14, new Color(137, 225, 255), true)
    drawText(g, "WINS " + current.wins + "   TAGS " + current.tags, 760, 44, 13, Mint, true)
  }

  private def drawLogin(g: Graphics2D, w: Int): Unit = {
    drawTitle(g, w, 155)
    drawCentered(g, "A NEW ARCADE BATTLE BEGINS", w / 2, 265, 17, White, true)
    drawCentered(g, "Create a local profile or log in to continue.", w / 2, 294, 14, Muted)
    val last = AppRoot.get("lastId", "")
    if (last.nonEmpty) drawCentered(g, "Last Player ID on this computer: " + last, w / 2, 330, 13, Mint)
    drawButton(g, "LOGIN", 350, 365, 300, 52, () => loginDialog())
    drawButton(g, "CREATE ACCOUNT", 350, 430, 300, 52, () => createAccountDialog())
    drawCentered(g, "Local profiles are saved on this computer.", w / 2, 520, 12, Muted)
    drawCentered(g, "Admin access is managed by the local game owner.", w / 2, 548, 12, new Color(255, 216, 105))
  }

  private def drawMenu(g: Graphics2D, w: Int): Unit = {
    drawProfileBar(g); drawTitle(g, w, 132)
    drawCentered(g, "Choose your next move, " + current.id + "!", w / 2, 235, 16, White, true)
    drawButton(g, "SINGLE PLAYER", 95, 280, 250, 55, () => screen = "modes")
    drawButton(g, "MULTIPLAYER", 375, 280, 250, 55, () => screen = "multiplayer")
    drawButton(g, "AVATAR + BUNDLE", 655, 280, 250, 55, () => screen = "avatar")
    drawButton(g, "POWER STYLES", 95, 350, 250, 55, () => screen = "powers")
    drawButton(g, "BUNDLE SHOP", 375, 350, 250, 55, () => screen = "shop")
    drawButton(g, "LEVEL REWARDS", 655, 350, 250, 55, () => screen = "levels")
    drawButton(g, "COSTUME + BLASTER CUSTOM", 65, 430, 270, 50, () => screen = "customize")
    if (adminMode) drawButton(g, "ADMIN PANEL", 365, 430, 220, 50, () => screen = "admin")
    drawButton(g, "LOG OUT", 635, 430, 220, 50, () => logout())
    drawCentered(g, lanStatus, w / 2, 535, 12, Mint)
    drawCentered(g, statusMessage, w / 2, 570, 13, Gold)
  }

  private def drawModes(g: Graphics2D, w: Int): Unit = {
    drawProfileBar(g); drawCentered(g, "SELECT YOUR BATTLE MODE", w / 2, 145, 30, Gold, true)
    drawCard(g, 90, 195, 250, 185, "BLOOM ROYALE", "Drop into Bloom Island.\nCollect star shards and tag Bloom Bots.")
    drawCard(g, 375, 195, 250, 185, "TAG SQUAD", "A quick team-tag\nround against Bloom Bots.")
    drawCard(g, 660, 195, 250, 185, "TRAINING GROUND", "Practice movement and\nBloom Pulse timing in a safe arena.")
    drawButton(g, "START BLOOM ROYALE", 90, 405, 250, 54, () => startMatch("BR"))
    drawButton(g, "START TAG SQUAD", 375, 405, 250, 54, () => startMatch("CS"))
    drawButton(g, "START TRAINING", 660, 405, 250, 54, () => startMatch("TRAINING"))
    drawButton(g, "BACK  (B)", 375, 510, 250, 48, () => screen = "menu")
    drawCentered(g, "ARROWS / W-A-D move   ?   S shoot Bloom orbs   ?   E loot   ?   Q power   ?   B back", w / 2, 590, 13, Muted)
  }


  private def drawMultiplayer(g: Graphics2D, w: Int): Unit = {
    drawProfileBar(g)
    drawCentered(g, "WI-FI MULTIPLAYER", w / 2, 150, 30, Gold, true)
    drawCard(g, 180, 205, 640, 115, "PLAY OVER YOUR LOCAL WI-FI", "One computer hosts a room. One other computer joins using the host IP and port.\nBoth computers must be on the same Wi-Fi network.")
    drawButton(g, "HOST A ROOM", 220, 370, 250, 58, () => hostLan())
    drawButton(g, "JOIN A ROOM", 530, 370, 250, 58, () => joinLan())
    drawCentered(g, lanStatus, w / 2, 475, 14, Mint, true)
    drawCentered(g, "This demo shares player markers; match progress stays local on each PC.", w / 2, 510, 12, Muted)
    drawButton(g, "BACK TO MENU", 385, 565, 230, 45, () => screen = "menu")
  }

  private def drawAvatar(g: Graphics2D, x: Int, y: Int, index: Int, scale: Int): Unit = {
    val c = index match { case 0 => new Color(111, 247, 173); case 1 => new Color(113, 199, 255); case 2 => new Color(206, 193, 255); case _ => new Color(255, 202, 103) }
    g.setColor(new Color(0, 0, 0, 70)); g.fillOval(x - scale, y + scale, scale * 2, scale / 2)
    g.setColor(c); g.fillOval(x - scale / 2, y - scale, scale, scale)
    g.setColor(new Color(29, 56, 67)); g.fillRoundRect(x - scale / 2, y, scale, scale, scale / 3, scale / 3)
    g.setColor(new Color(255, 255, 255, 200)); g.drawOval(x - scale / 2, y - scale, scale, scale)
  }

  private def drawPlayerAvatar(g: Graphics2D, x: Int, y: Int, index: Int, scale: Int): Unit = {
    drawAvatar(g, x, y, index, scale)
    val costumeColor = selectedCostume match {
      case 0 => new Color(95, 245, 170)
      case 1 => new Color(255, 135, 185)
      case 2 => new Color(140, 200, 255)
      case _ => new Color(255, 205, 95)
    }
    g.setColor(costumeColor)
    g.fillRoundRect(x - scale / 4, y + scale / 3, scale / 2, scale / 3, 8, 8)
  }

  private def drawAvatarScreen(g: Graphics2D, w: Int): Unit = {
    drawProfileBar(g); drawCentered(g, "CHOOSE YOUR AVATAR", w / 2, 140, 29, Gold, true)
    var i = 0
    while (i < avatarNames.length) {
      val x = 145 + i * 235
      drawCard(g, x - 82, 190, 164, 220, avatarNames(i), if (current.avatar == i) "CURRENT AVATAR" else "Select this hero")
      drawAvatar(g, x, 300, i, 46)
      val avatarIndex = i
      if (current.avatar == avatarIndex) drawCentered(g, "EQUIPPED", x, 385, 12, Mint, true)
      else drawButton(g, "SELECT", x - 65, 425, 130, 42, () => { current.avatar = avatarIndex; savePlayer(current); statusMessage = avatarNames(avatarIndex) + " selected." })
      i += 1
    }
    drawCard(g, 205, 490, 590, 68, "CURRENT BUNDLE: " + current.bundle, "Change bundles in the Bundle Shop.")
    drawButton(g, "BACK  (B)", 385, 585, 230, 44, () => screen = "menu")
  }

  private def drawShop(g: Graphics2D, w: Int): Unit = {
    drawProfileBar(g); drawCentered(g, "BUNDLE SHOP", w / 2, 145, 30, Gold, true)
    drawCard(g, 80, 200, 260, 180, "STARTER PETALS", "Free starter style\nIncluded with every account.")
    drawCard(g, 370, 200, 260, 180, "LEAF RANGER", "Green forest outfit\n150 coins")
    drawCard(g, 660, 200, 260, 180, "CRYSTAL BLOOM", "Crystal aura outfit\n260 coins + 25 diamonds")
    drawButton(g, "EQUIP STARTER", 80, 405, 260, 50, () => { current.bundle = bundles(0); savePlayer(current); statusMessage = "Starter Petals equipped." })
    drawButton(g, "BUY LEAF RANGER", 370, 405, 260, 50, () => buyBundle(1))
    drawButton(g, "BUY CRYSTAL BLOOM", 660, 405, 260, 50, () => buyBundle(2))
    drawCard(g, 300, 475, 400, 72, "SOLAR CHAMPION", "420 coins + 50 diamonds")
    drawButton(g, "BUY SOLAR", 385, 560, 230, 42, () => buyBundle(3))
    drawButton(g, "BACK  (B)", 30, 585, 180, 42, () => screen = "menu")
  }

  private def drawPowerStyles(g: Graphics2D, w: Int): Unit = {
    drawProfileBar(g); drawCentered(g, "POWER STYLE CUSTOMIZATION", w / 2, 145, 29, Gold, true)
    val n = profileNode(current.id)
    var i = 0
    while (i < powerNames.length) {
      val x = 85 + i * 225
      val owned = i == 0 || n.getBoolean("powerOwned_" + i, false)
      val price = if (powerDiamonds(i) > 0) powerDiamonds(i) + " diamonds" else powerCoins(i) + " coins"
      drawCard(g, x, 220, 190, 180, powerNames(i), if (owned) "OWNED" else "Cost: " + price)
      val powerColor = i match {
        case 0 => new Color(128, 255, 203)
        case 1 => new Color(94, 217, 153)
        case 2 => new Color(255, 196, 90)
        case _ => new Color(159, 199, 255)
      }
      g.setColor(powerColor)
      g.fillOval(x + 65, 280, 60, 60)
      drawCentered(g, if (current.powerStyle == powerNames(i)) "EQUIPPED" else if (owned) "EQUIP" else "UNLOCK", x + 95, 370, 12, Mint, true)
      val powerIndex = i
      drawButton(g, if (current.powerStyle == powerNames(powerIndex)) "EQUIPPED" else if (owned) "EQUIP" else "BUY / EQUIP", x + 8, 425, 174, 44, () => buyPowerStyle(powerIndex))
      i += 1
    }
    drawButton(g, "BACK  (B)", 385, 535, 230, 44, () => screen = "menu")
  }

  private def drawLevels(g: Graphics2D, w: Int): Unit = {
    drawProfileBar(g); drawCentered(g, "BLOOM LEVEL TRACK", w / 2, 140, 30, Gold, true)
    val xpNeed = current.xpTarget
    drawText(g, "LEVEL " + current.level + " / 20", 190, 220, 20, White, true)
    g.setColor(new Color(40, 76, 76)); g.fillRoundRect(190, 245, 620, 26, 14, 14)
    g.setColor(Mint); g.fillRoundRect(190, 245, (620.0 * current.xp / math.max(1, xpNeed)).toInt, 26, 14, 14)
    drawCentered(g, current.xp + " / " + xpNeed + " XP", 500, 290, 15, White, true)
    drawCard(g, 180, 330, 300, 120, "MATCH REWARDS", "Victory: 180 coins + XP\nMatch complete: 35 coins + XP")
    drawCard(g, 520, 330, 300, 120, "LEVEL REWARDS", "Level up: +75 coins\nMaximum level: 20")
    drawButton(g, "BACK  (B)", 385, 510, 230, 44, () => screen = "menu")
  }

  private def drawAdmin(g: Graphics2D, w: Int): Unit = {
    drawProfileBar(g); drawCentered(g, "ADMIN CONTROL PANEL", w / 2, 145, 30, Pink, true)
    drawCard(g, 195, 205, 610, 120, "ADMIN: BIXUBUX SHAH 25", "Search local Player IDs and grant test diamonds.\nThe profiles shown are saved on this computer only.")
    drawButton(g, "SEARCH PLAYER ID", 350, 365, 300, 55, () => adminSearch())
    drawButton(g, "SHOW LOCAL PLAYER COUNT", 350, 435, 300, 48, () => {
      val count = Try(Profiles.childrenNames().length).getOrElse(0)
      message("Local profiles on this computer: " + count)
    })
    drawButton(g, "BACK TO MENU", 385, 525, 230, 46, () => screen = "menu")
    drawCentered(g, "Admin can also play all game modes.", w / 2, 595, 13, Mint)
  }

  private def drawCustomizer(g: Graphics2D, w: Int): Unit = {
    drawProfileBar(g)
    drawCentered(g, "COSTUME + BLOOM BLASTER CUSTOMIZER", w / 2, 135, 27, Gold, true)
    drawCentered(g, "Choose a look. These are cosmetic styles, not real-world weapon designs.", w / 2, 163, 13, Muted)
    val costumeColors = Array(new Color(111, 247, 173), new Color(255, 153, 192), new Color(160, 205, 255), new Color(255, 205, 105))
    var i = 0
    while (i < costumeNames.length) {
      val x = 135 + i * 190
      drawCard(g, x - 72, 195, 145, 160, costumeNames(i), if (selectedCostume == i) "EQUIPPED LOOK" else "Free cosmetic look")
      drawAvatar(g, x, 275, current.avatar, 38)
      g.setColor(costumeColors(i)); g.fillOval(x - 13, 304, 26, 15)
      val costumeIndex = i
      drawButton(g, if (selectedCostume == costumeIndex) "EQUIPPED" else "EQUIP LOOK", x - 65, 365, 130, 40, () => {
        selectedCostume = costumeIndex
        profileNode(current.id).putInt("costume", selectedCostume)
        try { profileNode(current.id).flush() } catch { case _: Throwable => () }
        statusMessage = costumeNames(selectedCostume) + " equipped."
      })
      i += 1
    }
    drawCentered(g, "BLOOM PULSE STYLE", w / 2, 455, 20, Mint, true)
    i = 0
    while (i < blasterNames.length) {
      val x = 290 + i * 210
      val styleIndex = i
      drawButton(g, (if (selectedBlaster == styleIndex) "? " else "") + blasterNames(styleIndex), x - 85, 480, 170, 44, () => {
        selectedBlaster = styleIndex
        profileNode(current.id).putInt("blaster", selectedBlaster)
        try { profileNode(current.id).flush() } catch { case _: Throwable => () }
        statusMessage = blasterNames(selectedBlaster) + " visual style equipped."
      })
      i += 1
    }
    drawButton(g, "BACK  (B)", 385, 575, 230, 44, () => screen = "menu")
  }

  private def drawTree(g: Graphics2D, x: Int, y: Int, s: Int): Unit = {
    g.setColor(new Color(21, 65, 55, 140)); g.fillOval(x - s, y + s / 2, s * 2, s / 2)
    g.setColor(new Color(105, 73, 47)); g.fillRoundRect(x - 5, y, 10, s, 6, 6)
    g.setColor(new Color(26, 115, 76)); g.fillOval(x - s, y - s / 2, s * 2, s * 2)
    g.setColor(new Color(85, 210, 130)); g.fillOval(x - s / 2, y - s, s, s)
  }

  private def drawMap(g: Graphics2D): Unit = {
    g.setPaint(new GradientPaint(0, 65, new Color(51, 137, 101), 0, 660, new Color(24, 91, 73)))
    g.fillRect(0, 65, 1000, 595)
    g.setColor(new Color(174, 211, 132, 60))
    for (x <- 0 until 1000 by 60) g.drawLine(x, 65, x + 90, 660)
    for (y <- 100 until 660 by 60) g.drawLine(0, y, 1000, y + 12)
    // Small polygon platforms give the arena a layered, perspective-style look.
    val buildings = Array((125, 170, 145, 75), (690, 145, 150, 88), (760, 475, 130, 75), (160, 490, 145, 72), (430, 210, 130, 68))
    buildings.foreach { b =>
      val x = b._1; val y = b._2; val ww = b._3; val hh = b._4
      val p1 = new Polygon(Array(x, x + ww, x + ww + 24, x + 24), Array(y, y, y + 18, y + 18), 4)
      val p2 = new Polygon(Array(x + ww, x + ww + 24, x + ww + 24, x + ww), Array(y, y + 18, y + hh + 18, y + hh), 4)
      g.setColor(new Color(75, 106, 91)); g.fill(p1); g.setColor(new Color(47, 79, 73)); g.fill(p2)
      g.setColor(new Color(145, 187, 151)); g.fillRoundRect(x, y - 12, ww, hh, 12, 12)
      g.setColor(new Color(25, 75, 67)); g.drawRoundRect(x, y - 12, ww, hh, 12, 12)
    }
    drawTree(g, 65, 130, 27); drawTree(g, 900, 155, 30); drawTree(g, 60, 565, 30)
    drawTree(g, 925, 575, 28); drawTree(g, 560, 105, 23); drawTree(g, 350, 570, 24)
    g.setColor(new Color(114, 207, 220, 120)); g.fillOval(405, 455, 180, 48)
    g.setColor(new Color(196, 246, 238, 120)); g.drawOval(405, 455, 180, 48)
  }

  private def drawAircraft(g: Graphics2D, x: Int, y: Int): Unit = {
    // A decorative fantasy airship: no real aircraft controls or simulation.
    val body = new Polygon(Array(x - 48, x + 52, x + 32, x - 28), Array(y, y, y + 11, y + 11), 4)
    g.setColor(new Color(228, 245, 238)); g.fill(body)
    g.setColor(new Color(45, 92, 83)); g.draw(body)
    g.setColor(new Color(120, 215, 190)); g.fillPolygon(new Polygon(Array(x - 10, x + 8, x + 25, x - 27), Array(y - 2, y - 30, y - 30, y - 2), 4))
    g.setColor(new Color(255, 218, 115)); g.fillOval(x + 36, y + 2, 10, 7)
    g.setColor(new Color(180, 245, 226, 170)); g.drawLine(x - 34, y + 13, x - 58, y + 24)
  }

  private def drawBattle(g: Graphics2D, w: Int, h: Int): Unit = {
    drawMap(g)
    if (gameMode == "BR") {
      g.setColor(new Color(120, 245, 255, 45)); g.setStroke(new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1f, Array(12f, 8f), 0f))
      g.drawOval((500 - zoneRadius).toInt, (340 - zoneRadius).toInt, (zoneRadius * 2).toInt, (zoneRadius * 2).toInt)
      g.setStroke(new BasicStroke(1f))
    }
    loots.filterNot(_.taken).foreach { l =>
      val c = l.kind match { case "COINS" => Gold; case "DIAMOND" => new Color(120, 220, 255); case "ENERGY" => Pink; case _ => White }
      g.setColor(new Color(0, 0, 0, 65)); g.fillOval(l.x.toInt - 13, l.y.toInt + 7, 28, 12)
      g.setColor(c); g.fillRoundRect(l.x.toInt - 10, l.y.toInt - 15, 20, 20, 6, 6)
      g.setColor(Color.WHITE); g.drawRect(l.x.toInt - 10, l.y.toInt - 15, 20, 20)
    }
    bots.filter(_.alive).foreach { b =>
      drawAvatar(g, b.x.toInt, b.y.toInt, 2, 23)
      g.setColor(new Color(24, 65, 58)); g.fillRoundRect(b.x.toInt - 22, b.y.toInt - 42, 44, 6, 5, 5)
      g.setColor(Mint); g.fillRoundRect(b.x.toInt - 22, b.y.toInt - 42, (44.0 * (b.maxTags - b.tagsLeft) / b.maxTags).toInt, 6, 5, 5)
      drawCentered(g, "TAGS", b.x.toInt, b.y.toInt - 48, 9, White, true)
    }
    if (remotePlayer != null) {
      drawAvatar(g, remotePlayer.x.toInt, remotePlayer.y.toInt, remotePlayer.avatar, 27)
      drawCentered(g, remotePlayer.id, remotePlayer.x.toInt, remotePlayer.y.toInt - 42, 11, Color.WHITE, true)
    }
    if (!dropPhase) {
      drawPlayerAvatar(g, playerX.toInt, playerY.toInt, current.avatar, 29)
      drawCentered(g, current.id, playerX.toInt, playerY.toInt - 46, 12, Color.WHITE, true)
    }
    bloomBolts.foreach { bolt =>
      val boltColor = selectedBlaster match {
        case 0 => new Color(140, 255, 201)
        case 1 => new Color(145, 210, 255)
        case _ => new Color(255, 207, 115)
      }
      g.setColor(new Color(boltColor.getRed, boltColor.getGreen, boltColor.getBlue, 80))
      g.fillOval(bolt.x.toInt - 12, bolt.y.toInt - 12, 24, 24)
      g.setColor(boltColor); g.fillOval(bolt.x.toInt - 5, bolt.y.toInt - 5, 10, 10)
    }
    if (landingFlash > 0.0) {
      g.setColor(new Color(255, 255, 220, 150)); g.drawOval(playerX.toInt - 28, playerY.toInt - 28, 56, 56)
      g.setColor(new Color(255, 232, 156, 210))
      g.fillArc(playerX.toInt - 23, playerY.toInt - 62, 46, 34, 0, 180)
      g.setColor(new Color(245, 249, 235, 220))
      g.drawLine(playerX.toInt - 20, playerY.toInt - 43, playerX.toInt - 8, playerY.toInt - 4)
      g.drawLine(playerX.toInt + 20, playerY.toInt - 43, playerX.toInt + 8, playerY.toInt - 4)
    }
    // HUD
    g.setColor(new Color(3, 15, 24, 220)); g.fillRoundRect(18, 12, 965, 48, 14, 14)
    drawText(g, gameMode + "  ?  " + current.id, 32, 42, 14, White, true)
    drawText(g, "ENERGY " + playerEnergy + "/100", 260, 42, 14, Mint, true)
    drawText(g, "TAGS " + matchTags, 385, 42, 14, Gold, true)
    drawText(g, "TIME " + matchSeconds.toInt + "s", 500, 42, 14, White, true)
    drawText(g, "Q POWER", 625, 42, 13, Mint, true)
    drawText(g, "S SHOOT  ?  E LOOT  ?  ARROWS MOVE", 730, 42, 11, White, true)
    if (dropPhase) {
      drawAircraft(g, aircraftX.toInt, aircraftY.toInt)
      g.setColor(new Color(4, 20, 29, 215)); g.fillRoundRect(220, 205, 560, 190, 22, 22)
      drawCentered(g, "BLOOM AIRSHIP - BR DROP", 500, 245, 25, Gold, true)
      drawCentered(g, "1) Click the map to mark your landing point.", 500, 285, 15, White)
      drawCentered(g, "2) Press ENTER to jump and parachute down.", 500, 315, 15, Mint)
      drawCentered(g, "Marked: " + landingX.toInt + ", " + landingY.toInt, 500, 345, 13, White)
      g.setColor(new Color(255, 230, 125, 210)); g.setStroke(new BasicStroke(2f))
      g.drawOval(landingX.toInt - 17, landingY.toInt - 17, 34, 34)
      g.drawLine(landingX.toInt - 24, landingY.toInt, landingX.toInt + 24, landingY.toInt)
      g.drawLine(landingX.toInt, landingY.toInt - 24, landingX.toInt, landingY.toInt + 24)
      g.setStroke(new BasicStroke(1f))
      drawButton(g, "JUMP + LAND  (ENTER)", 375, 365, 250, 45, () => deployFromAirship())
    }
    if (matchFinished) {
      g.setColor(new Color(3, 16, 27, 225)); g.fillRoundRect(240, 205, 520, 250, 24, 24)
      drawCentered(g, if (matchWon) "BLOOM VICTORY!" else "MATCH OVER", 500, 265, 34, if (matchWon) Gold else Pink, true)
      drawCentered(g, "Tags: " + matchTags + "    Coins earned: " + (matchCoins + (if (matchWon) 180 else 35)), 500, 310, 16, White)
      drawCentered(g, "Your profile progress has been saved.", 500, 345, 14, Mint)
      drawButton(g, "BACK TO MENU", 375, 385, 250, 48, () => { screen = "menu"; closeLan() })
    } else {
      drawText(g, statusMessage, 22, 650, 12, White, true)
    }
  }

  private def paintGame(g0: Graphics): Unit = {
    val g = g0.asInstanceOf[Graphics2D]
    val w = canvas.getWidth; val h = canvas.getHeight
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
    buttons.clear()
    if (screen != "battle") drawBackground(g, w, h)
    screen match {
      case "login" => drawLogin(g, w)
      case "menu" => drawMenu(g, w)
      case "modes" => drawModes(g, w)
      case "multiplayer" => drawMultiplayer(g, w)
      case "avatar" => drawAvatarScreen(g, w)
      case "customize" => drawCustomizer(g, w)
      case "shop" => drawShop(g, w)
      case "powers" => drawPowerStyles(g, w)
      case "levels" => drawLevels(g, w)
      case "admin" => drawAdmin(g, w)
      case "battle" => drawBattle(g, w, h)
      case _ => screen = "menu"
    }
    g.dispose()
  }

  private class GamePanel extends JPanel {
    setPreferredSize(new Dimension(1000, 680))
    setFocusable(true)
    override def paintComponent(g: Graphics): Unit = { super.paintComponent(g); paintGame(g) }
  }

  private def installInput(): Unit = {
    canvas.addMouseListener(new MouseAdapter {
      override def mouseClicked(e: MouseEvent): Unit = {
        val found = buttons.reverse.find(_.rect.contains(e.getPoint))
        found match {
          case Some(b) => if (b.enabled) b.action()
          case None =>
            if (screen == "battle" && !matchFinished) {
              if (dropPhase) {
                landingX = clamp(e.getX, 30, 970)
                landingY = clamp(e.getY, 75, 620)
                statusMessage = "Landing point marked. Press ENTER when ready to jump."
              } else {
                aimX = e.getX.toDouble
                aimY = e.getY.toDouble
                shootBloomBolt()
              }
            }
        }
        canvas.repaint()
        canvas.requestFocusInWindow()
      }
    })
    canvas.addMouseMotionListener(new java.awt.event.MouseMotionAdapter {
      override def mouseMoved(e: MouseEvent): Unit = {
        if (screen == "battle" && !dropPhase) { aimX = e.getX.toDouble; aimY = e.getY.toDouble }
      }
    })
    canvas.addKeyListener(new KeyAdapter {
      override def keyPressed(e: KeyEvent): Unit = {
        heldKeys += e.getKeyCode
        if (e.getKeyCode == KeyEvent.VK_ENTER && screen == "battle" && dropPhase) deployFromAirship()
        else if (e.getKeyCode == KeyEvent.VK_ENTER && screen == "battle" && matchFinished) { screen = "menu"; closeLan() }
        else if (e.getKeyCode == KeyEvent.VK_E) collectNearby()
        else if (e.getKeyCode == KeyEvent.VK_Q) usePower()
        else if (e.getKeyCode == KeyEvent.VK_S) shootBloomBolt()
        else if (e.getKeyCode == KeyEvent.VK_SPACE) shootBloomBolt()
        else if (e.getKeyCode == KeyEvent.VK_B && screen != "login") { if (screen == "battle") gameRunning = false; screen = "menu" }
        else if (e.getKeyCode == KeyEvent.VK_ESCAPE && screen == "battle") { gameRunning = false; screen = "menu" }
        canvas.repaint()
      }
      override def keyReleased(e: KeyEvent): Unit = { heldKeys -= e.getKeyCode }
    })
  }

  private def launch(): Unit = {
    frame = new JFrame(AppTitle)
    canvas = new GamePanel
    installInput()
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
    frame.setContentPane(canvas)
    frame.pack()
    frame.setLocationRelativeTo(null)
    frame.setResizable(false)
    frame.setVisible(true)
    canvas.requestFocusInWindow()
    val timer = new Timer(33, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        updateGame()
        canvas.repaint()
      }
    })
    timer.start()
  }

  def main(args: Array[String]): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = {
        try { launch() }
        catch {
          case e: Throwable =>
            e.printStackTrace()
            JOptionPane.showMessageDialog(null, "Fighting Bloom startup error:\n" + e.toString, "Startup Error", JOptionPane.ERROR_MESSAGE)
        }
      }
    })
  }

  def run(): Unit = main(Array[String]())
}

// Kojo: run the whole script. This final line opens the Swing window.
BixuuSpringSeasonEvent6.main(Array[String]())