Code Sketch


bixuu
By: Mhalsakant School
Category: Programming
import java.awt.{AlphaComposite, BasicStroke, BorderLayout, CardLayout, Color, Component, Dimension, Font, Graphics, Graphics2D, GradientPaint, GraphicsEnvironment, Polygon, RenderingHints}
import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, MouseAdapter, MouseEvent, MouseMotionAdapter, MouseWheelListener, MouseWheelEvent, WindowAdapter, WindowEvent}
import java.io.{BufferedReader, InputStreamReader, PrintWriter}
import java.net.{Inet4Address, InetAddress, InetSocketAddress, NetworkInterface, ServerSocket, Socket}
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.{BorderFactory, Box, BoxLayout, JButton, JComboBox, JComponent, JFrame, JLabel, JOptionPane, JPanel, JPasswordField, JScrollPane, JTextArea, JTextField, SwingConstants, SwingUtilities, Timer, WindowConstants}
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.util.Try

/**
 * BIXUU SPRING SEASON - ULTRA ADVANCED 3D / LAN MULTIPLAYER EDITION
 * Scala 2.13, JDK 17+, Swing + software perspective polygon renderer.
 *
 * This is a genuine perspective-projected 3D scene rendered in Java2D, not a
 * photorealistic Unreal/Unity renderer. It uses 3D world coordinates, a movable
 * mouse-orbit camera, zoom, smooth movement physics, jump/sprint stamina, depth-sorted meshes, fog, directional lighting and weather overlays.
 */
object BixuuSpringSeasonMultiplayer {
  val APP_TITLE = "BIXUU - SPRING SEASON ULTRA 3D"
  val DEFAULT_PORT = 5050
  val MAX_PLAYERS = 10

  // Local prototype credentials. Do not expose this prototype directly to the Internet.
  private val ADMIN1_NAME = "Bixuu"
  private val ADMIN1_PIN = "BIXUU-2026"
  private val ADMIN2_NAME = "Admin2"
  private val ADMIN2_PIN = "yadbha2026"

  val WORLD_W = 100.0
  val WORLD_D = 84.0
  val WHITE = new Color(245, 251, 255)
  val MUTED = new Color(168, 193, 210)
  val CYAN = new Color(74, 238, 239)
  val MINT = new Color(135, 255, 190)
  val PINK = new Color(255, 117, 202)
  val PURPLE = new Color(176, 123, 255)
  val GOLD = new Color(255, 220, 124)
  val DARK = new Color(7, 15, 35)

  final case class V3(x: Double, y: Double, z: Double) {
    def +(b: V3): V3 = V3(x + b.x, y + b.y, z + b.z)
    def -(b: V3): V3 = V3(x - b.x, y - b.y, z - b.z)
    def *(s: Double): V3 = V3(x * s, y * s, z * s)
  }
  final case class Face(points: Vector[V3], color: Color)
  final case class Wire(points: Vector[V3], color: Color, width: Float)
  final case class ScreenPoint(x: Int, y: Int, depth: Double)
  final case class LevelInfo(name: String, subtitle: String, hue: Int, accent: Color, crystalCount: Int, guardCount: Int)
  final case class RemotePlayer(id: String, name: String, x: Double, z: Double)
  final class Crystal(val x: Double, val z: Double, val hue: Int, val label: String) { @volatile var collected = false }
  final class Tree(val x: Double, val z: Double, val scale: Double, val hue: Int)
  final class Flower(val x: Double, val z: Double, val scale: Double, val hue: Int, val phase: Double)
  final class Guard(val baseX: Double, val baseZ: Double, val radius: Double, val speed: Double, val phase: Double, val hue: Int) {
    var x = baseX; var z = baseZ; var alert = false
  }
  final class Particle(var x: Double, var y: Double, var z: Double, var vx: Double, var vy: Double, var vz: Double, val hue: Int, var life: Double, val size: Double)

  val levels = Vector(
    LevelInfo("WHISPERING WOODS", "Nature Realm - restore the lost spring seeds", 145, new Color(93, 224, 149), 5, 1),
    LevelInfo("AURORA FALLS", "Magic Realm - awaken the crystal waterfall", 190, new Color(91, 218, 245), 6, 2),
    LevelInfo("CELESTIAL TEMPLE", "Space Realm - unlock the ancient star gate", 270, new Color(188, 133, 255), 7, 3),
    LevelInfo("MONSOON GARDEN", "Weather Realm - bring rain back to the valley", 205, new Color(94, 176, 255), 7, 3),
    LevelInfo("BIO-TECH SANCTUARY", "Technology Realm - restart the Bloom Engine", 165, new Color(255, 184, 98), 8, 4)
  )

  val random = new java.util.Random(20261031L)
  val crystals = ArrayBuffer[Crystal]()
  val trees = ArrayBuffer[Tree]()
  val flowers = ArrayBuffer[Flower]()
  val guards = ArrayBuffer[Guard]()
  val particles = ArrayBuffer[Particle]()
  val completedLevels = mutable.Set[Int]()
  val extraEvents = ArrayBuffer[String]("GLITCH IN THE GARDEN", "AURORA BLOOM FESTIVAL")
  val pressedKeys = mutable.Set[Int]()

  @volatile var currentUser = "Player"
  @volatile var currentRole = "PLAYER"
  @volatile var isAdmin = false
  @volatile var isSuperAdmin = false
  @volatile var activeLevel = 0
  @volatile var multiplayerMode = "SOLO"
  @volatile var activeEventTitle = "AURORA FALLS: THE LOST SPRING"
  @volatile var announcement = "Welcome to Bixuu Spring Season!"
  @volatile var weatherMode = "SPRING BREEZE"

  var playerX = 8.0
  var playerZ = 72.0
  var cameraYaw = 0.0
  var cameraPitch = 0.24
  var cameraX = 0.0
  var cameraY = 0.0
  var cameraZ = 0.0
  var cameraDistance = 13.0
  var velocityX = 0.0
  var velocityZ = 0.0
  var jumpHeight = 0.0
  var jumpVelocity = 0.0
  var stamina = 100.0
  var frameCount = 0L
  var remainingSeconds = 420.0
  var health = 3
  var coins = 0
  var started = false
  var paused = false
  var won = false
  var lost = false
  var showGlow = true
  var showMiniMap = true
  var dashTime = 0.0
  var dashCooldown = 0.0
  var hitCooldown = 0.0
  var toast = "Explore the Spring Valley and collect every crystal."
  var toastTime = 4.0
  var mouseLastX = -1
  var mouseLastY = -1
  val portalX = 89.0
  val portalZ = 11.0
  val spawnX = 8.0
  val spawnZ = 72.0

  var frame: JFrame = _
  var root: JPanel = _
  var cardLayout: CardLayout = _
  var gamePanel: GamePanel = _
  var gameTimer: Timer = _
  var playerNameField: JTextField = _
  var adminNameChoice: JComboBox[String] = _
  var adminPasswordField: JPasswordField = _
  var adminButton: JButton = _
  var welcomeLabel: JLabel = _
  var levelButtons = ArrayBuffer[JButton]()
  var hostIpLabel: JLabel = _
  var networkStatus: JLabel = _
  var eventTitleField: JTextField = _
  var eventAddField: JTextField = _
  var announcementField: JTextField = _
  var eventListArea: JTextArea = _
  var weatherChoice: JComboBox[String] = _
  var lanServer: Option[LanServer] = None
  var lanClient: Option[LanClient] = None

  def clamp(v: Double, lo: Double, hi: Double): Double = math.max(lo, math.min(hi, v))
  def distance(x1: Double, z1: Double, x2: Double, z2: Double): Double = math.hypot(x1 - x2, z1 - z2)
  def safeName(s: String): String = Option(s).getOrElse("").trim.replaceAll("[,;|\\r\\n]", "_").take(18) match { case "" => "Player"; case v => v }
  def colorHue(h: Int, sat: Float = 0.72f, bri: Float = 1.0f): Color = Color.getHSBColor((h / 360.0).toFloat, sat, bri)
  def showToast(s: String, seconds: Double = 2.6): Unit = { toast = s; toastTime = seconds }
  def collectedCount: Int = crystals.count(_.collected)

  // Local progress is saved between runs. This is a local prototype save,
  // not a cloud account or a secure multiplayer database.
  private val progressFile = new java.io.File(
    System.getProperty("user.home"), ".bixuu_spring_ultra_progress.properties"
  )
  def loadProgress(): Unit = {
    if (progressFile.isFile) {
      val props = new java.util.Properties()
      var in: java.io.FileInputStream = null
      try {
        in = new java.io.FileInputStream(progressFile)
        props.load(in)
        coins = Try(props.getProperty("coins", "0").toInt).getOrElse(0).max(0)
        val saved = props.getProperty("completed", "")
        saved.split(",").filter(_.nonEmpty).foreach(v => Try(v.toInt).foreach(i => if(i>=0 && i<levels.length) completedLevels += i))
      } catch { case _: Throwable => () }
      finally { if (in != null) try in.close() catch { case _: Throwable => () } }
    }
  }
  def saveProgress(): Unit = {
    val props = new java.util.Properties()
    props.setProperty("coins", coins.toString)
    props.setProperty("completed", completedLevels.toVector.sorted.mkString(","))
    var out: java.io.FileOutputStream = null
    try {
      out = new java.io.FileOutputStream(progressFile)
      props.store(out, "Bixuu Spring Season local progress")
    } catch { case _: Throwable => showToast("Could not save local progress.", 2.0) }
    finally { if (out != null) try out.close() catch { case _: Throwable => () } }
  }

  // Colorful gradient buttons with a visible hover / press response.
  final class NeonButton(text: String, base: Color = new Color(16, 153, 142), edge: Color = CYAN) extends JButton(text) {
    private var hovering = false
    setFont(new Font("SansSerif", Font.BOLD, 14))
    setForeground(WHITE); setFocusPainted(false); setContentAreaFilled(false); setBorderPainted(false); setOpaque(false)
    setPreferredSize(new Dimension(300, 44)); setMaximumSize(new Dimension(390, 48)); setAlignmentX(Component.CENTER_ALIGNMENT)
    setBorder(BorderFactory.createEmptyBorder(10, 18, 10, 18))
    addMouseListener(new MouseAdapter {
      override def mouseEntered(e: MouseEvent): Unit = { hovering = true; repaint() }
      override def mouseExited(e: MouseEvent): Unit = { hovering = false; repaint() }
    })
    override protected def paintComponent(g0: Graphics): Unit = {
      val g = g0.create().asInstanceOf[Graphics2D]
      try {
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        val c1 = if (hovering) edge.brighter() else base.brighter()
        val c2 = if (hovering) base.brighter() else base.darker()
        g.setPaint(new GradientPaint(0, 0, c1, 0, getHeight, c2))
        g.fillRoundRect(1, 1, getWidth - 2, getHeight - 2, 18, 18)
        g.setColor(new Color(edge.getRed, edge.getGreen, edge.getBlue, if (hovering) 235 else 170))
        g.setStroke(new BasicStroke(if (hovering) 2.3f else 1.2f))
        g.drawRoundRect(1, 1, getWidth - 3, getHeight - 3, 18, 18)
        if (getModel.isPressed) { g.setColor(new Color(0, 0, 0, 45)); g.fillRoundRect(1, 1, getWidth - 2, getHeight - 2, 18, 18) }
      } finally g.dispose()
      super.paintComponent(g0)
    }
  }
  def styledButton(text: String, color: Color = new Color(16, 153, 142), edge: Color = CYAN): JButton = new NeonButton(text, color, edge)
  def heading(text: String, size: Int, color: Color): JLabel = { val l = new JLabel(text, SwingConstants.CENTER); l.setFont(new Font("SansSerif", Font.BOLD, size)); l.setForeground(color); l.setAlignmentX(Component.CENTER_ALIGNMENT); l }
  def bodyLabel(text: String): JLabel = { val l = new JLabel(text, SwingConstants.CENTER); l.setFont(new Font("SansSerif", Font.PLAIN, 13)); l.setForeground(MUTED); l.setAlignmentX(Component.CENTER_ALIGNMENT); l }
  def fieldStyle(f: JTextField): Unit = { f.setFont(new Font("SansSerif", Font.PLAIN, 14)); f.setBackground(new Color(14, 30, 57)); f.setForeground(WHITE); f.setCaretColor(CYAN); f.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(68, 185, 180)), BorderFactory.createEmptyBorder(7, 9, 7, 9))) }
  def verticalPanel(): JPanel = { val p = new JPanel(); p.setOpaque(true); p.setBackground(new Color(7, 17, 39)); p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); p.setBorder(BorderFactory.createEmptyBorder(22, 32, 22, 32)); p }
  def addCentered(p: JPanel, c: JComponent, gap: Int = 9): Unit = { p.add(Box.createVerticalStrut(gap)); c.setAlignmentX(Component.CENTER_ALIGNMENT); p.add(c) }
  def showCard(name: String): Unit = { cardLayout.show(root, name); root.revalidate(); root.repaint() }
  def addAction(b: JButton)(action: => Unit): Unit = b.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = action })
  def localIPv4: String = {
    val found = ArrayBuffer[String]()
    try {
      val en = NetworkInterface.getNetworkInterfaces
      while (en.hasMoreElements) { val ni = en.nextElement(); if (ni.isUp && !ni.isLoopback) { val ae = ni.getInetAddresses; while (ae.hasMoreElements) { val a = ae.nextElement(); if (a.isInstanceOf[Inet4Address] && !a.isLoopbackAddress) found += a.getHostAddress } } }
    } catch { case _: Throwable => () }
    found.headOption.getOrElse("127.0.0.1")
  }

  // ------------------------- LAN server -------------------------
  final class LanServer(port: Int, hostName: String, level: Int) {
    private val socket = new ServerSocket()
    private val running = new AtomicBoolean(true)
    private val nextId = new java.util.concurrent.atomic.AtomicInteger(1)
    private val sessions = new ConcurrentHashMap[String, ServerPeer]()
    private val sharedGems = ConcurrentHashMap.newKeySet[String]()
    @volatile private var hostX = spawnX
    @volatile private var hostZ = spawnZ
    private class ServerPeer(val id: String, val name: String, val sock: Socket, val out: PrintWriter) { @volatile var x = spawnX; @volatile var z = spawnZ }
    socket.setReuseAddress(true); socket.bind(new InetSocketAddress(port))
    def start(): Unit = {
      val acceptThread = new Thread(new Runnable { override def run(): Unit = {
        while (running.get()) try { val s = socket.accept(); s.setTcpNoDelay(true); val t = new Thread(new Runnable { override def run(): Unit = handle(s) }); t.setDaemon(true); t.start() } catch { case _: java.net.SocketException => (); case _: Throwable => () }
      }}, "Bixuu-LAN-Accept")
      acceptThread.setDaemon(true); acceptThread.start()
      val broadcastThread = new Thread(new Runnable { override def run(): Unit = {
        while (running.get()) { broadcastState(); try Thread.sleep(90L) catch { case _: InterruptedException => Thread.currentThread.interrupt() } }
      }}, "Bixuu-LAN-Broadcast")
      broadcastThread.setDaemon(true); broadcastThread.start()
    }
    private def handle(s: Socket): Unit = {
      var peer: ServerPeer = null
      try {
        val in = new BufferedReader(new InputStreamReader(s.getInputStream, "UTF-8"))
        val out = new PrintWriter(s.getOutputStream, true)
        val first = in.readLine()
        if (first == null) { s.close(); return }
        val p = first.split("\\|", -1)
        if (p.length < 3 || p(0) != "JOIN") { out.println("ERROR|Bad join packet"); s.close(); return }
        val name = safeName(p(1)); val requestedLevel = Try(p(2).toInt).getOrElse(-1)
        if (requestedLevel != level) { out.println("ERROR|Room level does not match"); s.close(); return }
        if (sessions.size() >= MAX_PLAYERS - 1) { out.println("ERROR|Room is full (10 players maximum)"); s.close(); return }
        val id = name + "-" + nextId.getAndIncrement()
        peer = new ServerPeer(id, name, s, out); sessions.put(id, peer)
        out.println("WELCOME|" + id)
        var line = in.readLine()
        while (line != null && running.get()) {
          if (line.startsWith("POS|")) {
            val xy = line.substring(4).split(",", 2)
            if (xy.length == 2) { val x = Try(xy(0).toDouble).getOrElse(peer.x); val z = Try(xy(1).toDouble).getOrElse(peer.z); peer.x = clamp(x, 0.0, WORLD_W); peer.z = clamp(z, 0.0, WORLD_D) }
          } else if (line.startsWith("GEM|")) {
            val i = Try(line.substring(4).toInt).getOrElse(-1)
            if (i >= 0 && i < 12) sharedGems.add(i.toString)
          }
          line = in.readLine()
        }
      } catch { case _: Throwable => () }
      finally { if (peer != null) sessions.remove(peer.id); try s.close() catch { case _: Throwable => () } }
    }
    def publishHostPosition(x: Double, z: Double): Unit = { hostX = clamp(x, 0, WORLD_W); hostZ = clamp(z, 0, WORLD_D) }
    def markGem(i: Int): Unit = if (i >= 0 && i < 12) sharedGems.add(i.toString)
    def collectedGems: Set[Int] = { val b = mutable.Set[Int](); val it = sharedGems.iterator(); while (it.hasNext) Try(it.next().toInt).foreach(i => { b += i; () }); b.toSet }
    def remotePlayers: Vector[RemotePlayer] = { val b = Vector.newBuilder[RemotePlayer]; val it = sessions.values().iterator(); while (it.hasNext) { val p = it.next(); b += RemotePlayer(p.id, p.name, p.x, p.z) }; b.result() }
    private def stateLine: String = {
      val all = ArrayBuffer[String]("HOST," + safeName(hostName) + "," + hostX + "," + hostZ)
      val it = sessions.values().iterator(); while (it.hasNext) { val p = it.next(); all += (p.id + "," + p.name + "," + p.x + "," + p.z) }
      "STATE|" + all.mkString(";") + "|" + sharedGems.toArray.mkString(",")
    }
    private def broadcastState(): Unit = { val msg = stateLine; val it = sessions.values().iterator(); while (it.hasNext) { val p = it.next(); try p.out.synchronized { p.out.println(msg) } catch { case _: Throwable => () } } }
    def close(): Unit = { running.set(false); try socket.close() catch { case _: Throwable => () }; val it = sessions.values().iterator(); while (it.hasNext) try it.next().sock.close() catch { case _: Throwable => () }; sessions.clear(); sharedGems.clear() }
  }

  // ------------------------- LAN client -------------------------
  final class LanClient(host: String, port: Int, playerName: String, level: Int) {
    private val socket = new Socket(); socket.connect(new InetSocketAddress(host, port), 4500); socket.setTcpNoDelay(true)
    private val out = new PrintWriter(socket.getOutputStream, true)
    private val in = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
    private val players = new ConcurrentHashMap[String, RemotePlayer]()
    private val gems = ConcurrentHashMap.newKeySet[String]()
    @volatile private var connected = true
    out.println("JOIN|" + safeName(playerName) + "|" + level)
    private val welcome = in.readLine()
    if (welcome == null || welcome.startsWith("ERROR|")) { socket.close(); throw new java.io.IOException(if (welcome == null) "Server closed the connection" else welcome.substring(6)) }
    private val id = if (welcome.startsWith("WELCOME|")) welcome.substring(8) else safeName(playerName)
    private val reader = new Thread(new Runnable { override def run(): Unit = {
      try { var line = in.readLine(); while (line != null && connected) {
        if (line.startsWith("STATE|")) {
          val sections = line.split("\\|", 3); players.clear()
          if (sections.length >= 2 && sections(1).nonEmpty) sections(1).split(";").foreach { e => val q = e.split(",", 4); if (q.length == 4) { val rp = RemotePlayer(q(0), q(1), Try(q(2).toDouble).getOrElse(0.0), Try(q(3).toDouble).getOrElse(0.0)); if (rp.id != id) players.put(rp.id, rp) } }
          gems.clear(); if (sections.length == 3 && sections(2).nonEmpty) sections(2).split(",").foreach(gems.add)
        }
        line = in.readLine()
      }} catch { case _: Throwable => () }; connected = false
    }}, "Bixuu-LAN-Reader")
    reader.setDaemon(true); reader.start()
    def isConnected: Boolean = connected
    def sendPosition(x: Double, z: Double): Unit = if (connected) out.synchronized { out.println("POS|" + x + "," + z) }
    def collectGem(i: Int): Unit = if (connected && i >= 0 && i < 12) out.synchronized { out.println("GEM|" + i) }
    def collectedGems: Set[Int] = { val b = mutable.Set[Int](); val it = gems.iterator(); while (it.hasNext) Try(it.next().toInt).foreach(i => { b += i; () }); b.toSet }
    def remotePlayers: Vector[RemotePlayer] = { val b = Vector.newBuilder[RemotePlayer]; val it = players.values().iterator(); while (it.hasNext) b += it.next(); b.result() }
    def close(): Unit = { connected = false; try socket.close() catch { case _: Throwable => () } }
  }

  // ------------------------- World setup -------------------------
  def terrainHeight(x: Double, z: Double): Double = {
    val rolling = math.sin(x * 0.16) * 0.38 + math.cos(z * 0.13) * 0.28 + math.sin((x + z) * 0.09) * 0.22
    val ridges = math.sin(x * 0.075 + z * 0.11) * 0.52 + math.cos(z * 0.055 - x * 0.04) * 0.34
    val detail = 0.12 * math.sin(x * 0.48) * math.cos(z * 0.36)
    val realmShape = activeLevel match {
      case 1 => math.sin(x * 0.12) * math.cos(z * 0.18) * 0.20
      case 2 => math.sin((x + z) * 0.17) * 0.25
      case 3 => math.cos(x * 0.20) * 0.13
      case 4 => math.sin(x * 0.27) * math.sin(z * 0.22) * 0.18
      case _ => math.sin(z * 0.08) * 0.12
    }
    rolling + ridges * 0.38 + detail + realmShape
  }
  def setupLevel(level: Int): Unit = {
    activeLevel = level.max(0).min(levels.length - 1)
    crystals.clear(); trees.clear(); flowers.clear(); guards.clear(); particles.clear(); pressedKeys.clear()
    val gemNames = Vector("MINT SEED", "SUN PETAL", "STAR BLOOM", "MOON GEM", "SPRING ORB", "AURORA HEART", "RAIN CRYSTAL", "TECH SEED")
    val count = levels(activeLevel).crystalCount
    var i = 0
    while (i < count) { val x = 17.0 + ((i * 13.7 + 9.0) % 72.0); val z = 63.0 - ((i * 9.3 + 4.0) % 50.0); crystals += new Crystal(x, z, Vector(145, 48, 190, 285, 315, 205, 170, 28)(i % 8), gemNames(i % gemNames.length)); i += 1 }
    i = 0
    while (i < 44) { val x = 3.0 + random.nextDouble() * 94.0; val z = 4.0 + random.nextDouble() * 76.0; if (distance(x, z, spawnX, spawnZ) > 8 && distance(x, z, portalX, portalZ) > 8) trees += new Tree(x, z, 0.72 + random.nextDouble() * 0.72, (levels(activeLevel).hue + random.nextInt(34) - 17 + 360) % 360); i += 1 }
    i = 0
    while (i < 320) { flowers += new Flower(random.nextDouble() * WORLD_W, random.nextDouble() * WORLD_D, 0.55 + random.nextDouble() * 0.75, Vector(145, 165, 188, 285, 315, 48)(random.nextInt(6)), random.nextDouble() * 6.28); i += 1 }
    i = 0
    while (i < levels(activeLevel).guardCount) { val x = 28.0 + i * 17.0; val z = 37.0 + (i % 2) * 16.0; guards += new Guard(x, z, 7.0 + i, 0.8 + i * 0.12, i * 1.7, if (i % 2 == 0) 330 else 275); i += 1 }
    playerX = spawnX; playerZ = spawnZ; cameraYaw = 0.0; cameraPitch = 0.52; cameraDistance = 13.0
    velocityX = 0.0; velocityZ = 0.0; jumpHeight = 0.0; jumpVelocity = 0.0; stamina = 100.0
    frameCount = 0L; remainingSeconds = 420.0; health = 3
    started = true; paused = false; won = false; lost = false; dashTime = 0; dashCooldown = 0; hitCooldown = 0; showGlow = true; showMiniMap = true
    showToast("" + levels(activeLevel).name + " - collect every crystal and reach the portal.", 4.0)
  }
  def startGame(level: Int, mode: String): Unit = {
    multiplayerMode = mode; setupLevel(level)
    if (gameTimer != null) gameTimer.stop()
    if (gamePanel != null) root.remove(gamePanel)
    gamePanel = new GamePanel(); root.add(gamePanel, "game"); showCard("game")
    gameTimer = new Timer(16, new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { updateGame(0.016); if (gamePanel != null) gamePanel.repaint() } })
    gameTimer.start(); gamePanel.requestFocusInWindow()
  }
  def closeNetwork(): Unit = { lanClient.foreach(_.close()); lanClient = None; lanServer.foreach(_.close()); lanServer = None }
  def returnToLevels(): Unit = { if (gameTimer != null) gameTimer.stop(); closeNetwork(); showCard("levels"); refreshLevelButtons() }
  def restartCurrentLevel(): Unit = { setupLevel(activeLevel); if (gamePanel != null) gamePanel.requestFocusInWindow() }

  def spawnBurst(x: Double, y: Double, z: Double, hue: Int, amount: Int): Unit = { var i = 0; while (i < amount) { val a = random.nextDouble() * math.Pi * 2; val s = 1.5 + random.nextDouble() * 4.5; particles += new Particle(x, y, z, math.cos(a) * s, random.nextDouble() * 3.5, math.sin(a) * s, hue, 0.6 + random.nextDouble() * 1.1, 0.06 + random.nextDouble() * 0.09); i += 1 } }
  def collectOrUse(): Unit = {
    if (!started || paused || won || lost) return
    val nearest = crystals.filter(!_.collected).sortBy(c => distance(playerX, playerZ, c.x, c.z)).headOption
    nearest match {
      case Some(c) if distance(playerX, playerZ, c.x, c.z) < 2.5 =>
        val idx = crystals.indexOf(c); c.collected = true; coins += 25; saveProgress()
        if (multiplayerMode == "HOST") lanServer.foreach(_.markGem(idx))
        if (multiplayerMode == "JOIN") lanClient.foreach(_.collectGem(idx))
        showToast("CRYSTAL FOUND: " + c.label + "  [" + collectedCount + "/" + crystals.length + "]", 2.5); spawnBurst(c.x, 1.4, c.z, c.hue, 28)
        if (collectedCount == crystals.length) showToast("ALL CRYSTALS FOUND! Reach the portal.", 4.0)
      case _ => if (distance(playerX, playerZ, portalX, portalZ) < 3.6) {
        if (collectedCount == crystals.length) { won = true; completedLevels += activeLevel; coins += 150; saveProgress(); showToast("REALM COMPLETE! +150 BLOOM COINS", 8.0); spawnBurst(portalX, 2.0, portalZ, 160, 90) }
        else showToast("PORTAL LOCKED - collect all crystals first.", 2.8)
      } else showToast("Move closer to a crystal or portal, then press E.", 2.4)
    }
  }

  def updateGame(dt: Double): Unit = {
    frameCount += 1
    if (toastTime > 0) toastTime = math.max(0, toastTime - dt)
    if (dashTime > 0) dashTime = math.max(0, dashTime - dt)
    if (dashCooldown > 0) dashCooldown = math.max(0, dashCooldown - dt)
    if (hitCooldown > 0) hitCooldown = math.max(0, hitCooldown - dt)
    particles.foreach { p => p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt; p.vy -= 2.4 * dt; p.life -= dt }
    particles.filter(_.life <= 0).toVector.foreach(particles -= _)
    if (!started || paused || won || lost) return
    remainingSeconds = math.max(0, remainingSeconds - dt)
    if (remainingSeconds <= 0) { lost = true; showToast("TIME OVER! Press R to retry.", 8.0); return }
    val forwardX = math.sin(cameraYaw); val forwardZ = math.cos(cameraYaw)
    val rightX = math.cos(cameraYaw); val rightZ = -math.sin(cameraYaw)
    var ix = 0.0; var iz = 0.0
    if (pressedKeys.contains(KeyEvent.VK_W) || pressedKeys.contains(KeyEvent.VK_UP)) { ix += forwardX; iz += forwardZ }
    if (pressedKeys.contains(KeyEvent.VK_S) || pressedKeys.contains(KeyEvent.VK_DOWN)) { ix -= forwardX; iz -= forwardZ }
    if (pressedKeys.contains(KeyEvent.VK_A) || pressedKeys.contains(KeyEvent.VK_LEFT)) { ix -= rightX; iz -= rightZ }
    if (pressedKeys.contains(KeyEvent.VK_D) || pressedKeys.contains(KeyEvent.VK_RIGHT)) { ix += rightX; iz += rightZ }
    val len = math.hypot(ix, iz)
    if (len > 0.001) { ix /= len; iz /= len }
    val sprinting = pressedKeys.contains(KeyEvent.VK_SHIFT) && stamina > 1.0 && len > 0.0
    val targetSpeed = if (dashTime > 0) 13.0 else if (sprinting) 8.4 else 5.4
    val tx = ix * targetSpeed; val tz = iz * targetSpeed
    val accel = if (len > 0.001) 8.5 else 5.2
    velocityX += (tx - velocityX) * math.min(1.0, accel * dt)
    velocityZ += (tz - velocityZ) * math.min(1.0, accel * dt)
    if (len <= 0.001) { velocityX *= math.max(0.0,1.0-dt*4.0); velocityZ *= math.max(0.0,1.0-dt*4.0) }
    playerX = clamp(playerX + velocityX * dt, 1.5, WORLD_W - 1.5)
    playerZ = clamp(playerZ + velocityZ * dt, 1.5, WORLD_D - 1.5)
    if (sprinting) stamina = math.max(0.0, stamina - 25.0 * dt) else stamina = math.min(100.0, stamina + 16.0 * dt)
    if (jumpHeight > 0.0 || jumpVelocity > 0.0) { jumpHeight += jumpVelocity * dt; jumpVelocity -= 15.5 * dt; if (jumpHeight <= 0.0) { jumpHeight = 0.0; jumpVelocity = 0.0 } }
    guards.foreach { g =>
      val before = distance(playerX, playerZ, g.x, g.z)
      val detection = if (sprinting || dashTime > 0) 20.0 else 15.0
      g.alert = before < detection
      if (g.alert) { val dx = playerX - g.x; val dz = playerZ - g.z; val n = math.max(0.001, math.hypot(dx, dz)); val chase = g.speed * (if(before<6)3.6 else 3.0); g.x += dx / n * chase * dt; g.z += dz / n * chase * dt }
      else { g.x = g.baseX + math.sin(frameCount * 0.018 * g.speed + g.phase) * g.radius; g.z = g.baseZ + math.cos(frameCount * 0.014 + g.phase) * 2.0 }
      val after = distance(playerX, playerZ, g.x, g.z)
      if (hitCooldown <= 0 && after < 1.35 && jumpHeight < 0.75 && dashTime <= 0) { health -= 1; hitCooldown = 1.8; playerX = clamp(playerX - (g.x - playerX) * 1.2, 1.5, WORLD_W - 1.5); playerZ = clamp(playerZ - (g.z - playerZ) * 1.2, 1.5, WORLD_D - 1.5); velocityX *= -0.45; velocityZ *= -0.45; showToast("A SHADOW GUARD CAUGHT YOU! Hearts: " + health, 2.0); spawnBurst(playerX, 1.2, playerZ, 330, 18); if (health <= 0) { lost = true; showToast("ADVENTURE OVER! Press R to retry.", 8.0) } }
    }
    multiplayerMode match {
      case "HOST" => lanServer.foreach { s => s.collectedGems.foreach(i => if (i >= 0 && i < crystals.length) crystals(i).collected = true); s.publishHostPosition(playerX, playerZ) }
      case "JOIN" => lanClient.foreach { c => c.collectedGems.foreach(i => if (i >= 0 && i < crystals.length) crystals(i).collected = true); c.sendPosition(playerX, playerZ) }
      case _ => ()
    }
  }

  // ------------------------- 3D math / renderer -------------------------
  def addCube(out: ArrayBuffer[Face], cx: Double, cy: Double, cz: Double, sx: Double, sy: Double, sz: Double, c: Color): Unit = {
    val x0 = cx - sx / 2; val x1 = cx + sx / 2; val y0 = cy - sy / 2; val y1 = cy + sy / 2; val z0 = cz - sz / 2; val z1 = cz + sz / 2
    val p000 = V3(x0,y0,z0); val p001 = V3(x0,y0,z1); val p010 = V3(x0,y1,z0); val p011 = V3(x0,y1,z1)
    val p100 = V3(x1,y0,z0); val p101 = V3(x1,y0,z1); val p110 = V3(x1,y1,z0); val p111 = V3(x1,y1,z1)
    out += Face(Vector(p000,p001,p011,p010), c.darker()); out += Face(Vector(p100,p110,p111,p101), c.brighter())
    out += Face(Vector(p000,p100,p101,p001), c.darker().darker()); out += Face(Vector(p010,p011,p111,p110), c.brighter())
    out += Face(Vector(p000,p010,p110,p100), c.darker()); out += Face(Vector(p001,p101,p111,p011), c)
  }
  def addSphere(out: ArrayBuffer[Face], center: V3, rx: Double, ry: Double, rz: Double, base: Color, rings: Int = 5, segments: Int = 8): Unit = {
    def point(r: Int, s: Int): V3 = { val lat = -math.Pi / 2 + math.Pi * r / rings; val lon = math.Pi * 2 * s / segments; V3(center.x + math.cos(lat) * math.cos(lon) * rx, center.y + math.sin(lat) * ry, center.z + math.cos(lat) * math.sin(lon) * rz) }
    var r = 0; while (r < rings) { var s = 0; while (s < segments) { val a = point(r,s); val b = point(r,s+1); val c = point(r+1,s+1); val d = point(r+1,s); val factor = (0.72 + 0.28 * (r.toDouble / rings)); val col = new Color((base.getRed*factor).toInt.min(255), (base.getGreen*factor).toInt.min(255), (base.getBlue*factor).toInt.min(255)); out += Face(Vector(a,b,c,d), col); s += 1 }; r += 1 }
  }
  def addCrystalMesh(out: ArrayBuffer[Face], x: Double, z: Double, hue: Int): Unit = {
    val y = terrainHeight(x,z) + 1.35; val c = colorHue(hue); val top = V3(x,y+0.85,z); val bottom = V3(x,y-0.72,z)
    val ring = Vector(V3(x-0.48,y,z),V3(x,y,z+0.48),V3(x+0.48,y,z),V3(x,y,z-0.48))
    var i=0; while(i<4){ val a=ring(i); val b=ring((i+1)%4); out += Face(Vector(top,a,b),c.brighter()); out += Face(Vector(bottom,b,a),c.darker()); i+=1 }
  }
  def addTreeMesh(out: ArrayBuffer[Face], t: Tree): Unit = {
    val gy = terrainHeight(t.x,t.z); addCube(out,t.x,gy+1.55*t.scale,t.z,0.38*t.scale,3.1*t.scale,0.38*t.scale,new Color(104,67,43))
    addSphere(out,V3(t.x,gy+3.5*t.scale,t.z),1.45*t.scale,1.55*t.scale,1.35*t.scale,colorHue(t.hue,0.65f,0.88f),5,7)
    addSphere(out,V3(t.x-0.72*t.scale,gy+3.0*t.scale,t.z+0.15),0.85*t.scale,0.95*t.scale,0.84*t.scale,colorHue((t.hue+12)%360,0.62f,0.9f),4,6)
  }
  def addHumanMesh(out: ArrayBuffer[Face], x: Double, z: Double, cloth: Color, local: Boolean, bob: Double = 0, lift: Double = 0): Unit = {
    val y = terrainHeight(x,z) + lift
    val moving = local && (pressedKeys.contains(KeyEvent.VK_W) || pressedKeys.contains(KeyEvent.VK_A) || pressedKeys.contains(KeyEvent.VK_S) || pressedKeys.contains(KeyEvent.VK_D))
    val stride = if (moving) math.sin(frameCount * 0.28) * 0.16 else 0.0
    addCube(out,x,y+0.95+bob,z,0.72,1.12,0.48,cloth)
    addSphere(out,V3(x,y+1.83+bob,z),0.37,0.39,0.36,new Color(237,190,151),5,8)
    addCube(out,x-0.22,y+0.36+stride,z,0.20,0.72,0.22,new Color(35,48,76)); addCube(out,x+0.22,y+0.36-stride,z,0.20,0.72,0.22,new Color(35,48,76))
    addCube(out,x-0.22,y+0.02+stride,z+0.08,0.34,0.16,0.48,new Color(47,220,210)); addCube(out,x+0.22,y+0.02-stride,z+0.08,0.34,0.16,0.48,new Color(47,220,210))
    addCube(out,x-0.48,y+1.05+bob-stride,z,0.20,0.78,0.22,cloth.darker()); addCube(out,x+0.48,y+1.05+bob+stride,z,0.20,0.78,0.22,cloth.darker())
    addCube(out,x,y+2.18+bob,z,0.70,0.14,0.60,new Color(20,55,88))
    addCube(out,x-0.13,y+1.87+bob,z+0.31,0.07,0.06,0.04,new Color(25,40,58)); addCube(out,x+0.13,y+1.87+bob,z+0.31,0.07,0.06,0.04,new Color(25,40,58))
  }

  def addGuardianMesh(out: ArrayBuffer[Face], g: Guard): Unit = {
    val y = terrainHeight(g.x,g.z)
    val gait = math.sin(frameCount * (if(g.alert) 0.42 else 0.20) + g.phase) * (if(g.alert) 0.22 else 0.10)
    val body = colorHue(g.hue,0.68f,if(g.alert)0.96f else 0.78f)
    addSphere(out,V3(g.x,y+0.92,g.z),0.86,0.62,1.20,body,5,8)
    addSphere(out,V3(g.x,y+1.38,g.z+0.76),0.57,0.49,0.60,body.brighter(),5,8)
    addSphere(out,V3(g.x,y+1.22,g.z+1.18),0.40,0.27,0.33,new Color(45,34,59),4,7)
    // pointed ears and glowing eyes
    val earL=V3(g.x-0.36,y+2.05,g.z+0.70); val earR=V3(g.x+0.36,y+2.05,g.z+0.70)
    out += Face(Vector(earL,V3(g.x-0.62,y+1.42,g.z+0.68),V3(g.x-0.12,y+1.48,g.z+0.78)),body.darker())
    out += Face(Vector(earR,V3(g.x+0.62,y+1.42,g.z+0.68),V3(g.x+0.12,y+1.48,g.z+0.78)),body.darker())
    addSphere(out,V3(g.x-0.22,y+1.48,g.z+1.18),0.09,0.09,0.06,GOLD,3,5)
    addSphere(out,V3(g.x+0.22,y+1.48,g.z+1.18),0.09,0.09,0.06,GOLD,3,5)
    addSphere(out,V3(g.x,y+0.92,g.z-1.23),0.16,0.16,0.70,body.darker(),4,6)
    addCube(out,g.x-0.48,y+0.34+gait,g.z+0.70,0.22,0.68,0.24,body.darker())
    addCube(out,g.x+0.48,y+0.34-gait,g.z+0.70,0.22,0.68,0.24,body.darker())
    addCube(out,g.x-0.48,y+0.34-gait,g.z-0.68,0.22,0.68,0.24,body.darker())
    addCube(out,g.x+0.48,y+0.34+gait,g.z-0.68,0.22,0.68,0.24,body.darker())
  }
  def addPortalMesh(out: ArrayBuffer[Face], wires: ArrayBuffer[Wire]): Unit = {
    val baseY = terrainHeight(portalX,portalZ); val center = V3(portalX,baseY+2.6,portalZ); val major=2.05; val tube=0.18; val segments=28; val sides=6
    var i=0; while(i<segments){ val a0=i*math.Pi*2/segments; val a1=(i+1)*math.Pi*2/segments; val p0=V3(center.x+math.cos(a0)*major,center.y,center.z); val p1=V3(center.x+math.cos(a1)*major,center.y,center.z); wires += Wire(Vector(V3(p0.x,center.y-major,p0.z),V3(p1.x,center.y-major,p1.z)),if(i%2==0) CYAN else PURPLE,4.0f); var j=0; while(j<sides){ val u0=j*math.Pi*2/sides; val u1=(j+1)*math.Pi*2/sides; def q(a:Double,u:Double)=V3(center.x+(major+tube*math.cos(u))*math.cos(a),center.y+tube*math.sin(u),center.z+(major+tube*math.cos(u))*math.sin(a)); out += Face(Vector(q(a0,u0),q(a1,u0),q(a1,u1),q(a0,u1)),if(i%2==0) new Color(76,224,238) else new Color(151,104,255)); j+=1 }; i+=1 }
    addCube(out,portalX,baseY+0.05,portalZ,5.4,0.18,3.0,new Color(61,91,125))
  }
  def cameraBasis(): (V3,V3,V3,V3) = {
    val forwardPlanar = V3(math.sin(cameraYaw), 0, math.cos(cameraYaw))
    val horizontal = cameraDistance * math.cos(cameraPitch)
    val vertical = cameraDistance * math.sin(cameraPitch) + 1.0
    val ground = terrainHeight(playerX, playerZ)
    val cam = V3(playerX - forwardPlanar.x * horizontal, ground + vertical, playerZ - forwardPlanar.z * horizontal)
    val target = V3(playerX, ground + 1.25 + jumpHeight * 0.30, playerZ)
    val f0 = target - cam
    val fl = math.max(0.0001, math.sqrt(f0.x*f0.x + f0.y*f0.y + f0.z*f0.z))
    val f = V3(f0.x/fl, f0.y/fl, f0.z/fl)
    val up0 = V3(0,1,0)
    val r0 = V3(up0.y*f.z-up0.z*f.y, up0.z*f.x-up0.x*f.z, up0.x*f.y-up0.y*f.x)
    val rl = math.max(0.0001, math.sqrt(r0.x*r0.x+r0.y*r0.y+r0.z*r0.z))
    val r = V3(r0.x/rl,r0.y/rl,r0.z/rl)
    val u = V3(f.y*r.z-f.z*r.y,f.z*r.x-f.x*r.z,f.x*r.y-f.y*r.x)
    cameraX=cam.x; cameraY=cam.y; cameraZ=cam.z
    (cam,f,r,u)
  }
  def project(v: V3, cam: V3, f: V3, r: V3, u: V3, w: Int, h: Int): Option[ScreenPoint] = {
    val d=v-cam; val z=d.x*f.x+d.y*f.y+d.z*f.z
    if (z <= 0.45) None else { val cx=d.x*r.x+d.y*r.y+d.z*r.z; val cy=d.x*u.x+d.y*u.y+d.z*u.z; val focal=math.min(w,h)*0.88; Some(ScreenPoint((w*0.5+cx*focal/z).toInt,(h*0.52-cy*focal/z).toInt,z)) }
  }
  def litColor(base: Color, points: Vector[V3]): Color = {
    if (points.length < 3) return base
    val a = points(1) - points(0)
    val b = points(2) - points(0)
    val nx = a.y*b.z - a.z*b.y
    val ny = a.z*b.x - a.x*b.z
    val nz = a.x*b.y - a.y*b.x
    val nlen = math.max(0.0001, math.sqrt(nx*nx + ny*ny + nz*nz))
    val light = V3(-0.38, 0.84, -0.39)
    val diffuse = math.max(0.0, (nx*light.x + ny*light.y + nz*light.z) / nlen)
    val factor = 0.48 + diffuse * 0.58
    new Color(
      clamp(base.getRed * factor, 0, 255).toInt,
      clamp(base.getGreen * factor, 0, 255).toInt,
      clamp(base.getBlue * factor, 0, 255).toInt
    )
  }
  def fogColor(depth: Double, source: Color): Color = {
    val sky = colorHue((levels(activeLevel).hue + 210) % 360, 0.48f, 0.48f)
    val t = clamp((depth - 23.0) / 82.0, 0.0, 0.64)
    new Color(
      (source.getRed*(1-t)+sky.getRed*t).toInt,
      (source.getGreen*(1-t)+sky.getGreen*t).toInt,
      (source.getBlue*(1-t)+sky.getBlue*t).toInt
    )
  }

  def createSceneFaces(): (ArrayBuffer[Face],ArrayBuffer[Wire]) = {
    val faces=ArrayBuffer[Face](); val wires=ArrayBuffer[Wire]()
    // Curved ground is built from real 3D triangles, not a flat screen-space fill.
    val minX=math.max(0,(playerX-35).toInt/5*5).toDouble; val maxX=math.min(WORLD_W,(playerX+35).toInt/5*5).toDouble
    val minZ=math.max(0,(playerZ-42).toInt/5*5).toDouble; val maxZ=math.min(WORLD_D,(playerZ+42).toInt/5*5).toDouble
    var x=minX; while(x<maxX){ var z=minZ; while(z<maxZ){ val x1=x+5; val z1=z+5; val h00=terrainHeight(x,z); val h10=terrainHeight(x1,z); val h11=terrainHeight(x1,z1); val h01=terrainHeight(x,z1); val band=if(((x/5).toInt+(z/5).toInt)%2==0) 1.0 else 0.92; val base=levels(activeLevel).accent; val c=new Color((base.getRed*band*0.58).toInt.min(255),(base.getGreen*band*0.72).toInt.min(255),(base.getBlue*band*0.60).toInt.min(255)); faces += Face(Vector(V3(x,h00,z),V3(x1,h10,z),V3(x1,h11,z1)),c); faces += Face(Vector(V3(x,h00,z),V3(x1,h11,z1),V3(x,h01,z1)),c.brighter()); z+=5 }; x+=5 }
    trees.foreach(t=>if(distance(playerX,playerZ,t.x,t.z)<47) addTreeMesh(faces,t))
    crystals.filter(!_.collected).foreach(c=>if(distance(playerX,playerZ,c.x,c.z)<48) addCrystalMesh(faces,c.x,c.z,c.hue))
    guards.foreach { g => if(distance(playerX,playerZ,g.x,g.z)<42) addGuardianMesh(faces,g) }
    // Dark contact shadow anchors the player to the terrain even while jumping.
    addSphere(faces,V3(playerX,terrainHeight(playerX,playerZ)+0.035,playerZ),0.62,0.025,0.46,new Color(25,42,47),3,9)
    addPortalMesh(faces,wires)
    addHumanMesh(faces,playerX,playerZ,new Color(27,139,160),true,math.sin(frameCount*0.16)*0.04,jumpHeight)
    val others = if(multiplayerMode=="HOST") lanServer.toVector.flatMap(_.remotePlayers) else if(multiplayerMode=="JOIN") lanClient.toVector.flatMap(_.remotePlayers) else Vector.empty
    others.foreach(p=>if(distance(playerX,playerZ,p.x,p.z)<45) addHumanMesh(faces,p.x,p.z,new Color(211,119,69),false))
    // Flower stems / petals as low-poly colored objects.
    flowers.foreach { fl => if(distance(playerX,playerZ,fl.x,fl.z)<25){ val y=terrainHeight(fl.x,fl.z); addCube(faces,fl.x,y+0.24*fl.scale,fl.z,0.045,0.48*fl.scale,0.045,new Color(47,126,65)); addSphere(faces,V3(fl.x,y+0.52*fl.scale,fl.z),0.20*fl.scale,0.12*fl.scale,0.20*fl.scale,colorHue(fl.hue),3,6) } }
    // Distant waterfall cliff in the eastern valley.
    addCube(faces,78,5.0,27,12,10,2,new Color(92,112,126)); addCube(faces,78,3.6,25.8,2.0,7.8,0.18,new Color(94,205,245)); addCube(faces,78,0.25,25.8,11,0.22,4,new Color(54,156,193))
    (faces,wires)
  }
  def draw3DWorld(g: Graphics2D,w:Int,h:Int): Unit = {
    val top=colorHue((levels(activeLevel).hue+210)%360,0.64f,0.21f); val bottom=new Color(10,48,65)
    g.setPaint(new GradientPaint(0,0,top,0,h,bottom)); g.fillRect(0,0,w,h)
    // Distant sky stars / drifting spring motes.
    var i=0; while(i<85){ val px=(i*97+(frameCount/3).toInt)%(w.max(1)); val py=(i*43)%math.max(1,(h*0.43).toInt); g.setColor(new Color(235,250,255,90+(i%5)*25)); g.fillOval(px,py,2+(i%3),2+(i%3)); i+=1 }
    val (cam,f,r,u)=cameraBasis(); val (faces,wires)=createSceneFaces()
    val projected=faces.flatMap { face =>
      val ps=face.points.map(project(_,cam,f,r,u,w,h))
      if(ps.forall(_.nonEmpty)) {
        val q=ps.flatten
        val depth=q.map(_.depth).sum/q.length
        Some((depth,q,fogColor(depth,litColor(face.color,face.points))))
      } else None
    }.sortBy(item => -item._1)
    projected.foreach { item =>
      val q=item._2
      val poly=new Polygon(q.map(_.x).toArray,q.map(_.y).toArray,q.length)
      g.setColor(item._3); g.fillPolygon(poly)
      g.setColor(new Color(255,255,255,24)); g.setStroke(new BasicStroke(0.55f)); g.drawPolygon(poly)
    }
    wires.foreach { line => val p=line.points.flatMap(project(_,cam,f,r,u,w,h)); if(p.length>=2){ g.setColor(line.color); g.setStroke(new BasicStroke(line.width,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND)); for(i<-0 until p.length-1) g.drawLine(p(i).x,p(i).y,p(i+1).x,p(i+1).y) } }
    // Crystal labels are screen-space annotations above the 3D gems.
    crystals.filter(!_.collected).foreach { c => project(V3(c.x,terrainHeight(c.x,c.z)+2.5,c.z),cam,f,r,u,w,h).foreach { p => if(p.x>0&&p.x<w&&p.y>0&&p.y<h){ g.setFont(new Font("SansSerif",Font.BOLD,10)); g.setColor(colorHue(c.hue)); g.drawString(c.label,p.x-28,p.y) } } }
    particles.foreach { p => project(V3(p.x,p.y,p.z),cam,f,r,u,w,h).foreach { q => val alpha=clamp(p.life/1.7,0,1).toFloat; val old=g.getComposite; g.setComposite(AlphaComposite.SrcOver.derive(alpha)); g.setColor(colorHue(p.hue)); val size=math.max(2,(p.size*math.max(0.35,8.0/q.depth)*12).toInt); g.fillOval(q.x-size/2,q.y-size/2,size,size); g.setComposite(old) } }
  }
  def drawWeatherOverlay(g: Graphics2D, w: Int, h: Int): Unit = {
    weatherMode match {
      case "SUNNY BLOOM" =>
        val sx = w - 220; val sy = 122
        val pulse = 1.0 + math.sin(frameCount * 0.025) * 0.04
        val r = (38 * pulse).toInt
        val old = g.getComposite
        g.setComposite(AlphaComposite.SrcOver.derive(0.22f)); g.setColor(GOLD); g.fillOval(sx-r*2,sy-r*2,r*4,r*4)
        g.setComposite(AlphaComposite.SrcOver.derive(0.88f)); g.fillOval(sx-r,sy-r,r*2,r*2); g.setComposite(old)
        g.setColor(new Color(255,231,157,125)); for(i <- 0 until 12){val a=i*math.Pi/6;g.drawLine(sx+(math.cos(a)*r*1.35).toInt,sy+(math.sin(a)*r*1.35).toInt,sx+(math.cos(a)*r*1.75).toInt,sy+(math.sin(a)*r*1.75).toInt)}
      case "RAINY MONSOON" =>
        g.setColor(new Color(155,220,255,125)); var i=0; while(i<115){val x=(i*83+(frameCount*5).toInt)%(w.max(1));val y=(i*47+(frameCount*11).toInt)%(h.max(1));g.drawLine(x,y,x-7,y+19);i+=1}
        g.setColor(new Color(110,168,202,30));g.fillRect(0,0,w,h)
      case "AURORA NIGHT" =>
        val old=g.getComposite; g.setComposite(AlphaComposite.SrcOver.derive(0.18f))
        for(i <- 0 until 5){g.setColor(if (i % 2 == 0) MINT else PURPLE);val yy=78+i*19+(math.sin(frameCount*0.018+i)*13).toInt;g.setStroke(new BasicStroke(8.0f+i));g.drawArc(w/3,yy,w/2,90,8+i*4,150)}
        g.setComposite(old)
      case "STARFALL" =>
        g.setColor(new Color(255,231,176,190));var i=0;while(i<24){val x=(i*151+(frameCount*4).toInt)%(w.max(1));val y=(i*67+(frameCount*7).toInt)%(math.max(1,h/2));g.drawLine(x,y,x-15,y+9);g.fillOval(x-2,y-2,4,4);i+=1}
      case _ =>
        val old=g.getComposite;g.setComposite(AlphaComposite.SrcOver.derive(0.58f));var i=0;while(i<32){val x=(i*127+(frameCount*(1+i%3)).toInt)%(w.max(1));val y=(i*71+(frameCount*2).toInt)%(h.max(1));g.setColor(colorHue(Vector(145,165,188,285,315,48)(i%6),0.55f,1.0f));g.fillOval(x,y,3+i%4,4+i%5);i+=1};g.setComposite(old)
    }
  }

  def drawMiniMap(g: Graphics2D,w:Int): Unit = {
    val x=w-190; val y=105; val mw=170; val mh=120; g.setColor(new Color(4,12,28,220)); g.fillRoundRect(x,y,mw,mh,14,14); g.setColor(CYAN); g.drawRoundRect(x,y,mw,mh,14,14)
    def mx(v:Double)=x+8+(v/WORLD_W*(mw-16)).toInt; def mz(v:Double)=y+8+(v/WORLD_D*(mh-16)).toInt
    crystals.filter(!_.collected).foreach(c=>{g.setColor(colorHue(c.hue));g.fillOval(mx(c.x)-2,mz(c.z)-2,5,5)})
    guards.foreach(v=>{g.setColor(PINK);g.fillOval(mx(v.x)-3,mz(v.z)-3,6,6)})
    g.setColor(MINT);g.fillOval(mx(portalX)-4,mz(portalZ)-4,8,8);g.setColor(WHITE);g.fillOval(mx(playerX)-4,mz(playerZ)-4,8,8)
    g.setFont(new Font("SansSerif",Font.BOLD,9));g.drawString("3D REALM MAP",x+9,y+13)
  }
  def drawHUD(g:Graphics2D,w:Int,h:Int):Unit={
    g.setColor(new Color(4,13,31,220));g.fillRoundRect(14,13,445,105,16,16);g.setColor(new Color(79,229,219,180));g.drawRoundRect(14,13,445,105,16,16)
    g.setFont(new Font("SansSerif",Font.BOLD,17));g.setColor(CYAN);g.drawString("BIXUU / SPRING SEASON 3D",27,39)
    g.setFont(new Font("SansSerif",Font.BOLD,11));g.setColor(WHITE);g.drawString("REALM: "+levels(activeLevel).name,27,60)
    g.setColor(GOLD);g.drawString("CRYSTALS: "+collectedCount+" / "+crystals.length+"     COINS: "+coins,27,80)
    g.setColor(PINK);g.drawString("HEARTS: "+("? "*health),27,100);g.setColor(MINT);g.fillRoundRect(300,94,125,7,6,6);g.setColor(new Color(36,60,75));g.fillRoundRect(300,94,125,7,6,6);g.setColor(MINT);g.fillRoundRect(300,94,(125.0*stamina/100.0).toInt,7,6,6);g.setColor(WHITE);g.setFont(new Font("SansSerif",Font.PLAIN,8));g.drawString("STAMINA",300,89)
    val mm=remainingSeconds.toInt/60;val ss=remainingSeconds.toInt%60;g.setColor(new Color(4,13,31,220));g.fillRoundRect(w-160,15,145,57,12,12);g.setFont(new Font("Monospaced",Font.BOLD,21));g.setColor(if(remainingSeconds<40)PINK else GOLD);g.drawString("%02d:%02d".format(mm,ss),w-142,43);g.setFont(new Font("SansSerif",Font.PLAIN,9));g.setColor(WHITE);g.drawString("TIME LEFT",w-140,60)
    if(showMiniMap)drawMiniMap(g,w)
    g.setColor(new Color(4,13,31,215));g.fillRoundRect(14,h-39,w-28,27,10,10);g.setFont(new Font("SansSerif",Font.BOLD,10));g.setColor(MINT);g.drawString("WASD MOVE  SHIFT SPRINT  SPACE JUMP  Q DASH  E COLLECT  M MAP  F GLOW  ESC PAUSE",25,h-21)
    g.setColor(CYAN);g.drawString(multiplayerMode+"  |  "+currentUser+"  |  "+weatherMode, w-385,h-21)
    if(toastTime>0){val tw=math.min(w-40,math.max(320,g.getFontMetrics.stringWidth(toast)+34));val tx=(w-tw)/2;g.setColor(new Color(4,14,32,235));g.fillRoundRect(tx,h-83,tw,30,12,12);g.setColor(CYAN);g.drawRoundRect(tx,h-83,tw,30,12,12);g.setColor(WHITE);g.setFont(new Font("SansSerif",Font.BOLD,11));g.drawString(toast,tx+15,h-64)}
  }
  def drawOverlay(g:Graphics2D,w:Int,h:Int):Unit={g.setColor(new Color(3,8,20,205));g.fillRect(0,0,w,h);val title=if(won)"REALM RESTORED!" else if(lost)"ADVENTURE OVER" else "PAUSED";g.setFont(new Font("SansSerif",Font.BOLD,38));g.setColor(if(won)MINT else if(lost)PINK else CYAN);g.drawString(title,w/2-g.getFontMetrics.stringWidth(title)/2,h/2-35);g.setFont(new Font("SansSerif",Font.PLAIN,15));g.setColor(WHITE);val sub=if(won)"Spring energy restored. Great work, "+currentUser+"!" else if(lost)"Try again and avoid the Shadow Guards." else "Your 3D adventure is paused.";g.drawString(sub,w/2-g.getFontMetrics.stringWidth(sub)/2,h/2);g.setFont(new Font("SansSerif",Font.BOLD,14));g.setColor(GOLD);val hint=if(won)"ENTER: NEXT REALM     R: LEVEL SELECT"else"ESC: RESUME     R: RESTART / LEVEL SELECT";g.drawString(hint,w/2-g.getFontMetrics.stringWidth(hint)/2,h/2+42)}

  final class GamePanel extends JPanel {
    setPreferredSize(new Dimension(1180,760));setFocusable(true);setDoubleBuffered(true);setBackground(DARK)
    addKeyListener(new KeyAdapter {
      override def keyPressed(e: KeyEvent): Unit = {
        val k = e.getKeyCode
        val fresh = !pressedKeys.contains(k)
        pressedKeys += k
        if (fresh) {
          if (k == KeyEvent.VK_E) {
            collectOrUse()
          } else if (k == KeyEvent.VK_SPACE && started && !won && !lost && jumpHeight <= 0.02) {
            jumpVelocity = 6.4
          } else if (k == KeyEvent.VK_Q && started && !won && !lost && dashCooldown <= 0.0) {
            dashTime = 0.34
            dashCooldown = 1.1
          } else if (k == KeyEvent.VK_F) {
            showGlow = !showGlow
          } else if (k == KeyEvent.VK_M) {
            showMiniMap = !showMiniMap
          } else if (k == KeyEvent.VK_ESCAPE && !won && !lost) {
            paused = !paused
          } else if (k == KeyEvent.VK_R) {
            if (won || lost) returnToLevels()
            else if (paused) paused = false
            else returnToLevels()
          } else if (k == KeyEvent.VK_ENTER && won) {
            if (activeLevel < levels.length - 1) startGame(activeLevel + 1, "SOLO")
            else returnToLevels()
          }
        }
      }
      override def keyReleased(e: KeyEvent): Unit = {
        pressedKeys -= e.getKeyCode
      }
    })
    addMouseListener(new MouseAdapter{override def mousePressed(e:MouseEvent):Unit={mouseLastX=e.getX;mouseLastY=e.getY;requestFocusInWindow()};override def mouseReleased(e:MouseEvent):Unit={mouseLastX = -1;mouseLastY = -1}})
    addMouseMotionListener(new MouseMotionAdapter{override def mouseDragged(e:MouseEvent):Unit={if(mouseLastX>=0){val dx=e.getX-mouseLastX;val dy=e.getY-mouseLastY;cameraYaw+=dx*0.006;cameraPitch=clamp(cameraPitch+dy*0.0035,0.20,1.02);mouseLastX=e.getX;mouseLastY=e.getY;repaint()}}})
    addMouseWheelListener(new MouseWheelListener{override def mouseWheelMoved(e:MouseWheelEvent):Unit={cameraDistance=clamp(cameraDistance+e.getPreciseWheelRotation*0.85,8.0,21.0);repaint()}})
    override protected def paintComponent(graphics:Graphics):Unit={super.paintComponent(graphics);val g=graphics.create().asInstanceOf[Graphics2D];try{g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);draw3DWorld(g,getWidth,getHeight);drawWeatherOverlay(g,getWidth,getHeight);drawHUD(g,getWidth,getHeight);if(paused||won||lost)drawOverlay(g,getWidth,getHeight)}finally g.dispose()}
  }

  // ------------------------- Login screens -------------------------
  def loginChoiceScreen():JPanel={val p=verticalPanel();p.add(Box.createVerticalGlue());addCentered(p,heading("BIXUU",25,MINT),4);addCentered(p,heading("SPRING SEASON",42,CYAN),5);addCentered(p,heading("FIVE ELEMENTS  /  3D ADVENTURE",17,PINK),3);addCentered(p,bodyLabel("Choose your separate login type."),22);val player=styledButton("PLAYER LOGIN",new Color(15,139,124),MINT);val admin=styledButton("ADMIN LOGIN",new Color(115,61,160),PINK);addCentered(p,player,9);addCentered(p,admin,9);p.add(Box.createVerticalGlue());addAction(player){showCard("player")};addAction(admin){showCard("adminLogin")};p}
  def playerLoginScreen():JPanel={val p=verticalPanel();p.add(Box.createVerticalGlue());addCentered(p,heading("PLAYER LOGIN",31,CYAN),8);addCentered(p,bodyLabel("Enter any player name to enter the Spring World."),12);playerNameField=new JTextField("Player",20);fieldStyle(playerNameField);playerNameField.setMaximumSize(new Dimension(300,38));addCentered(p,playerNameField,7);val enter=styledButton("ENTER AS PLAYER",new Color(15,139,124),MINT);val back=styledButton("BACK",new Color(47,71,103),CYAN);addCentered(p,enter,14);addCentered(p,back,7);p.add(Box.createVerticalGlue());addAction(enter){currentUser=safeName(playerNameField.getText);currentRole="PLAYER";isAdmin=false;isSuperAdmin=false;updateWelcome();adminButton.setVisible(false);showCard("menu")};addAction(back){showCard("login")};p}
  def adminLoginScreen():JPanel={val p=verticalPanel();p.add(Box.createVerticalGlue());addCentered(p,heading("ADMIN LOGIN",31,GOLD),8);addCentered(p,bodyLabel("Admin access is separate from Player Login."),12);adminNameChoice=new JComboBox[String](Array("Bixuu Admin","Admin 2"));adminNameChoice.setMaximumSize(new Dimension(300,34));addCentered(p,adminNameChoice,7);adminPasswordField=new JPasswordField(20);fieldStyle(adminPasswordField);adminPasswordField.setMaximumSize(new Dimension(300,38));addCentered(p,adminPasswordField,7);val enter=styledButton("VERIFY ADMIN & ENTER",new Color(119,72,165),PINK);val back=styledButton("BACK",new Color(47,71,103),CYAN);addCentered(p,enter,14);addCentered(p,back,7);p.add(Box.createVerticalGlue());addAction(enter){val selected=adminNameChoice.getSelectedIndex;val pin=new String(adminPasswordField.getPassword);val ok=if(selected==0)pin==ADMIN1_PIN else pin==ADMIN2_PIN;if(ok){currentUser=if(selected==0)ADMIN1_NAME else ADMIN2_NAME;currentRole=if(selected==0)"SUPER ADMIN"else"ADMIN 2";isAdmin=true;isSuperAdmin=selected==0;adminPasswordField.setText("");updateWelcome();adminButton.setVisible(true);showCard("menu")}else{JOptionPane.showMessageDialog(frame,"Admin password is incorrect.","Access denied",JOptionPane.ERROR_MESSAGE);adminPasswordField.setText("")}};addAction(back){showCard("login")};p}
  def updateWelcome():Unit=if(welcomeLabel!=null)welcomeLabel.setText("Welcome, "+currentUser+"  /  "+currentRole)
  def mainMenuScreen():JPanel={val p=verticalPanel();p.add(Box.createVerticalGlue());addCentered(p,heading("BIXUU SPRING SEASON ULTRA 3D",31,CYAN),4);welcomeLabel=bodyLabel("Welcome, Player");p.add(welcomeLabel);val levels=styledButton("FIVE REALMS / ADVENTURES",new Color(13,137,112),MINT);val multi=styledButton("MULTIPLAYER - HOTSPOT / WI-FI",new Color(21,103,151),CYAN);adminButton=styledButton("ADMIN CONTROL CENTER",new Color(119,72,165),PINK);val logout=styledButton("LOG OUT",new Color(119,53,70),new Color(255,151,163));addCentered(p,levels,18);addCentered(p,multi,9);addCentered(p,adminButton,9);addCentered(p,logout,9);adminButton.setVisible(false);p.add(Box.createVerticalGlue());addAction(levels){refreshLevelButtons();showCard("levels")};addAction(multi){showCard("multiplayer")};addAction(adminButton){refreshAdminPanel();showCard("admin")};addAction(logout){closeNetwork();if(gameTimer!=null)gameTimer.stop();currentUser="Player";currentRole="PLAYER";isAdmin=false;isSuperAdmin=false;adminButton.setVisible(false);showCard("login")};p}
  def levelSelectScreen():JPanel={val p=verticalPanel();addCentered(p,heading("CHOOSE YOUR SPRING REALM",29,CYAN),5);addCentered(p,bodyLabel("Explore five realms, collect crystals, sprint, jump over danger and reach the portal."),5);levelButtons.clear();levels.zipWithIndex.foreach{case(l,i)=>val b=styledButton("REALM "+(i+1)+"  -  "+l.name,l.accent.darker(),l.accent);levelButtons+=b;addCentered(p,b,7);addCentered(p,bodyLabel(l.subtitle),2);addAction(b){if(i==0||completedLevels.contains(i-1))startGame(i,"SOLO")else JOptionPane.showMessageDialog(frame,"Complete the previous Realm first.")}};val back=styledButton("BACK TO MENU",new Color(47,71,103),CYAN);addCentered(p,back,12);p.add(Box.createVerticalGlue());addAction(back){showCard("menu")};refreshLevelButtons();p}
  def refreshLevelButtons():Unit={if(levelButtons.length==levels.length)levelButtons.zipWithIndex.foreach{case(b,i)=>b.setEnabled(i==0||completedLevels.contains(i-1));if(i > 0 && !b.isEnabled)b.setText("LOCKED - "+levels(i).name)}}
  def labelFor(s:String):JLabel={val l=new JLabel(s);l.setForeground(WHITE);l.setFont(new Font("SansSerif",Font.BOLD,12));l}

  // ------------------------- Multiplayer lobby -------------------------
  def multiplayerScreen():JPanel={val p=verticalPanel();addCentered(p,heading("MULTIPLAYER LOBBY",30,CYAN),4);addCentered(p,bodyLabel("LAN play: host one room, then friends join using the same Wi-Fi / hotspot."),4);hostIpLabel=bodyLabel("Host IP will appear here after hosting.");addCentered(p,hostIpLabel,10);val portField=new JTextField(DEFAULT_PORT.toString,7);fieldStyle(portField);val roomLevel=new JComboBox[String](levels.map(_.name).toArray);val hostRow=new JPanel();hostRow.setOpaque(false);hostRow.add(labelFor("Port"));hostRow.add(portField);hostRow.add(labelFor("Realm"));hostRow.add(roomLevel);p.add(hostRow);val hostBtn=styledButton("HOST ROOM",new Color(14,135,106),MINT);addCentered(p,hostBtn,8);val ipField=new JTextField("192.168.1.2",14);fieldStyle(ipField);val joinPort=new JTextField(DEFAULT_PORT.toString,7);fieldStyle(joinPort);val joinRow=new JPanel();joinRow.setOpaque(false);joinRow.add(labelFor("Host IPv4"));joinRow.add(ipField);joinRow.add(labelFor("Port"));joinRow.add(joinPort);p.add(joinRow);val joinBtn=styledButton("JOIN ROOM",new Color(21,102,154),CYAN);addCentered(p,joinBtn,8);networkStatus=bodyLabel("Status: not connected");addCentered(p,networkStatus,6);val back=styledButton("BACK TO MENU",new Color(47,71,103),CYAN);addCentered(p,back,8);p.add(Box.createVerticalGlue());addAction(hostBtn){try{val port=portField.getText.trim.toInt;require(port>=1024&&port<=65535,"Port must be between 1024 and 65535.");closeNetwork();val server=new LanServer(port,currentUser,roomLevel.getSelectedIndex);server.start();lanServer=Some(server);val ip=localIPv4;hostIpLabel.setText("HOST IP: "+ip+"    PORT: "+port);networkStatus.setText("Room hosted - waiting for players.");JOptionPane.showMessageDialog(frame,"Room hosted. Share this IP and port with friends:\n\n"+ip+":"+port+"\n\nAllow Java through Windows Firewall on Private networks.");startGame(roomLevel.getSelectedIndex,"HOST")}catch{case ex:Throwable=>JOptionPane.showMessageDialog(frame,"Could not host: "+ex.getMessage,"Host error",JOptionPane.ERROR_MESSAGE)}};addAction(joinBtn){try{val ip=ipField.getText.trim;val port=joinPort.getText.trim.toInt;require(ip.nonEmpty,"Enter the host IPv4 address.");closeNetwork();val client=new LanClient(ip,port,currentUser,roomLevel.getSelectedIndex);lanClient=Some(client);networkStatus.setText("Connected to "+ip+":"+port);startGame(roomLevel.getSelectedIndex,"JOIN")}catch{case ex:Throwable=>JOptionPane.showMessageDialog(frame,"Could not join: "+ex.getMessage+"\nCheck IP, port, same Wi-Fi and firewall.","Join error",JOptionPane.ERROR_MESSAGE)}};addAction(back){showCard("menu")};p}

  // ------------------------- Admin control center -------------------------
  def refreshAdminPanel():Unit={if(eventListArea!=null)eventListArea.setText((Vector("BUILT-IN: GLITCH IN THE GARDEN","BUILT-IN: "+activeEventTitle)++extraEvents.map("CUSTOM: "+_)).mkString("\n"));if(eventTitleField!=null)eventTitleField.setText(activeEventTitle)}
  def adminScreen():JPanel={val p=verticalPanel();addCentered(p,heading("ADMIN CONTROL CENTER",28,GOLD),4);addCentered(p,bodyLabel("Role: SUPER ADMIN has full local controls. Admin 2 has event controls."),4);val erow=new JPanel();erow.setOpaque(false);erow.add(labelFor("Event title"));eventTitleField=new JTextField(activeEventTitle,27);fieldStyle(eventTitleField);erow.add(eventTitleField);p.add(erow);val save=styledButton("SAVE EVENT TITLE",new Color(119,72,165),PINK);addCentered(p,save,7);val addrow=new JPanel();addrow.setOpaque(false);eventAddField=new JTextField(24);fieldStyle(eventAddField);addrow.add(labelFor("New event"));addrow.add(eventAddField);p.add(addrow);val add=styledButton("ADD EVENT",new Color(13,137,112),MINT);addCentered(p,add,7);val arow=new JPanel();arow.setOpaque(false);announcementField=new JTextField(25);fieldStyle(announcementField);arow.add(labelFor("Announcement"));arow.add(announcementField);p.add(arow);val announce=styledButton("BROADCAST ANNOUNCEMENT",new Color(21,102,154),CYAN);addCentered(p,announce,7);val wr=new JPanel();wr.setOpaque(false);weatherChoice=new JComboBox[String](Array("SPRING BREEZE","SUNNY BLOOM","RAINY MONSOON","AURORA NIGHT","STARFALL"));wr.add(labelFor("World weather"));wr.add(weatherChoice);p.add(wr);val applyWeather=styledButton("APPLY WORLD WEATHER",new Color(14,135,106),MINT);addCentered(p,applyWeather,7);eventListArea=new JTextArea(5,46);eventListArea.setEditable(false);eventListArea.setFont(new Font("Monospaced",Font.PLAIN,12));eventListArea.setBackground(new Color(4,13,30));eventListArea.setForeground(MINT);val sc=new JScrollPane(eventListArea);sc.setAlignmentX(Component.CENTER_ALIGNMENT);p.add(Box.createVerticalStrut(8));p.add(sc);val back=styledButton("BACK TO MENU",new Color(47,71,103),CYAN);addCentered(p,back,9);p.add(Box.createVerticalGlue());addAction(save){val v=eventTitleField.getText.trim;if(v.nonEmpty){activeEventTitle=v.take(64);refreshAdminPanel();JOptionPane.showMessageDialog(frame,"Event title updated for this session.")}};addAction(add){val v=eventAddField.getText.trim;if(v.nonEmpty && !extraEvents.contains(v)){extraEvents+=v.take(64);eventAddField.setText("");refreshAdminPanel()}};addAction(announce){val v=announcementField.getText.trim;if(v.nonEmpty){announcement=v.take(110);announcementField.setText("");showToast("ANNOUNCEMENT: "+announcement,5.0);JOptionPane.showMessageDialog(frame,"Announcement set: "+announcement)}};addAction(applyWeather){weatherMode=weatherChoice.getSelectedItem.toString;showToast("WORLD WEATHER: "+weatherMode,4.0)};addAction(back){showCard("menu")};refreshAdminPanel();p}

  def buildUI():Unit={
    loadProgress()
    cardLayout=new CardLayout();root=new JPanel(cardLayout);root.add(loginChoiceScreen(),"login");root.add(playerLoginScreen(),"player");root.add(adminLoginScreen(),"adminLogin");root.add(mainMenuScreen(),"menu");root.add(levelSelectScreen(),"levels");root.add(multiplayerScreen(),"multiplayer");root.add(adminScreen(),"admin")
    frame=new JFrame(APP_TITLE);frame.setContentPane(root);frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);frame.addWindowListener(new WindowAdapter{override def windowClosing(e:WindowEvent):Unit={if(gameTimer!=null)gameTimer.stop();closeNetwork();frame.dispose()}})
    frame.setMinimumSize(new Dimension(920,680));frame.setSize(1180,780);frame.setLocationRelativeTo(null);frame.setVisible(true);showCard("login")
  }
  def main(args:Array[String]):Unit=SwingUtilities.invokeLater(new Runnable{override def run():Unit=buildUI()})
}


BixuuSpringSeasonMultiplayer.main(Array[String]())