Code Sketch


bixuu02 but the make all game is yoi
By: Mhalsakant School
import java.awt._
import java.awt.event._
import java.awt.geom.AffineTransform
import java.awt.image.BufferedImage
import java.io._
import java.net._
import java.security.MessageDigest
import java.util.{Base64, Locale, Properties}
import javax.swing._
import javax.swing.border.EmptyBorder
import scala.collection.mutable
import scala.util.Random

/**
 * BIXUU ANIME FIGHTING CARDS - 3D LOOK EDITION
 * Scala + Java Swing/AWT; designed for Kojo environments without Scala Swing.
 * Visuals use Java2D perspective, glow, gradients and animation (not a true 3D engine).
 */
object BixuuCardsBattle {
  case class Fighter(name: String, power: Int, speed: Int, defense: Int,
                     chakra: Int, special: String, color: Color)

  class PlayerAccount(val username: String, val passwordHash: String) {
    var wins = 0
    var losses = 0
    var battles = 0
    val cards = mutable.ArrayBuffer[String]()
  }

  val random = new Random()
  val bg = new Color(7, 10, 25)
  val panelColor = new Color(18, 24, 47)
  val gold = new Color(255, 195, 65)
  val textColor = new Color(225, 235, 255)
  val purple = new Color(115, 75, 255)
  val seriesNames = Vector("Naruto Shippuden", "Demon Slayer", "Jujutsu Kaisen", "Pokemon")
  val stats = Vector("POWER", "SPEED", "DEFENSE", "CHAKRA")

  val narutoCards = Vector(
    Fighter("Naruto Uzumaki",96,94,90,99,"RASENGAN",new Color(255,140,30)),
    Fighter("Sasuke Uchiha",97,96,88,98,"CHIDORI",new Color(90,100,220)),
    Fighter("Sakura Haruno",86,80,94,78,"SUPER PUNCH",new Color(235,100,150)),
    Fighter("Kakashi Hatake",92,93,90,94,"LIGHTNING BLADE",new Color(80,155,195)),
    Fighter("Itachi Uchiha",98,95,90,99,"TSUKUYOMI",new Color(170,35,65)),
    Fighter("Madara Uchiha",100,96,99,100,"PERFECT SUSANOO",new Color(155,45,50)),
    Fighter("Obito Uchiha",97,92,96,98,"KAMUI",new Color(100,75,160)),
    Fighter("Pain",96,88,94,98,"ALMIGHTY PUSH",new Color(180,70,75)),
    Fighter("Minato Namikaze",95,100,87,96,"FLYING RAJIN",new Color(235,190,55)),
    Fighter("Gaara",90,78,99,92,"SAND SHIELD",new Color(175,105,75)),
    Fighter("Rock Lee",92,99,88,65,"LOTUS ATTACK",new Color(70,165,95)),
    Fighter("Shikamaru Nara",82,78,83,80,"SHADOW JUTSU",new Color(90,130,90)),
    Fighter("Jiraiya",94,85,92,96,"SAGE RASENGAN",new Color(200,95,45)),
    Fighter("Orochimaru",93,90,94,97,"SNAKE JUTSU",new Color(150,190,130)),
    Fighter("Neji Hyuga",88,92,85,88,"GENTLE FIST",new Color(180,195,210)),
    Fighter("Tsunade",94,79,98,90,"HEALING POWER",new Color(195,125,100))
  )

  val demonCards = Vector(
    Fighter("Tanjiro Kamado",94,93,88,94,"WATER BREATHING",new Color(50,135,180)),
    Fighter("Nezuko Kamado",93,91,92,90,"BLOOD DEMON ART",new Color(190,80,125)),
    Fighter("Zenitsu Agatsuma",96,100,78,83,"THUNDERCLAP",new Color(235,195,50)),
    Fighter("Inosuke Hashibira",92,96,85,80,"BEAST BREATHING",new Color(100,165,125)),
    Fighter("Giyu Tomioka",96,94,94,95,"WATER HASHIRA",new Color(65,100,185)),
    Fighter("Shinobu Kocho",88,97,81,91,"INSECT BREATHING",new Color(165,115,210)),
    Fighter("Kyojuro Rengoku",97,94,92,94,"FLAME BREATHING",new Color(225,100,35)),
    Fighter("Tengen Uzui",94,98,91,89,"SOUND BREATHING",new Color(180,180,190)),
    Fighter("Mitsuri Kanroji",93,91,89,90,"LOVE BREATHING",new Color(225,110,160)),
    Fighter("Muichiro Tokito",95,98,88,94,"MIST BREATHING",new Color(90,170,175)),
    Fighter("Akaza",98,96,96,93,"DESTRUCTIVE DEATH",new Color(195,90,100)),
    Fighter("Kokushibo",100,97,98,99,"MOON BREATHING",new Color(145,70,105))
  )

  val jujutsuCards = Vector(
    Fighter("Yuji Itadori",91,93,89,88,"BLACK FLASH",new Color(190,80,105)),
    Fighter("Megumi Fushiguro",91,92,86,94,"TEN SHADOWS",new Color(75,95,160)),
    Fighter("Nobara Kugisaki",87,88,85,87,"RESONANCE",new Color(195,115,65)),
    Fighter("Satoru Gojo",100,99,99,100,"INFINITY",new Color(90,165,230)),
    Fighter("Ryomen Sukuna",100,98,99,100,"MALEVOLENT SHRINE",new Color(170,65,70)),
    Fighter("Yuta Okkotsu",97,91,93,99,"CURSED ENERGY",new Color(115,155,190)),
    Fighter("Maki Zenin",92,98,91,72,"CURSED TOOLS",new Color(75,145,100)),
    Fighter("Toge Inumaki",88,89,80,96,"CURSED SPEECH",new Color(170,155,190)),
    Fighter("Kento Nanami",93,85,94,88,"OVERTIME",new Color(190,165,90)),
    Fighter("Mahito",96,94,91,98,"IDLE TRANSFIGURATION",new Color(145,100,175))
  )

  val pokemonCards = Vector(
    Fighter("Pikachu",88,100,70,87,"THUNDERBOLT",new Color(240,205,45)),
    Fighter("Charizard",97,92,91,95,"FIRE BLAST",new Color(235,100,50)),
    Fighter("Mewtwo",100,96,92,100,"PSYSTRIKE",new Color(145,115,190)),
    Fighter("Eevee",72,85,70,75,"QUICK ATTACK",new Color(180,135,95)),
    Fighter("Lucario",93,94,89,92,"AURA SPHERE",new Color(65,115,190)),
    Fighter("Greninja",94,100,82,91,"WATER SHURIKEN",new Color(65,145,200)),
    Fighter("Gengar",91,92,80,95,"SHADOW BALL",new Color(120,85,165)),
    Fighter("Dragonite",96,86,95,94,"DRAGON CLAW",new Color(225,150,75)),
    Fighter("Blastoise",93,78,98,93,"HYDRO PUMP",new Color(65,125,200)),
    Fighter("Venusaur",91,75,96,90,"SOLAR BEAM",new Color(85,160,100)),
    Fighter("Snorlax",88,48,100,85,"BODY SLAM",new Color(110,155,170)),
    Fighter("Garchomp",97,95,93,91,"DRAGON RUSH",new Color(110,115,180))
  )

  def cardsFor(series: String): Vector[Fighter] = {
    if (series == "Naruto Shippuden") narutoCards
    else if (series == "Demon Slayer") demonCards
    else if (series == "Jujutsu Kaisen") jujutsuCards
    else pokemonCards
  }
  def randomFighter(series: String): Fighter = {
    val set = cardsFor(series); set(random.nextInt(set.size))
  }
  def findFighter(series: String, name: String): Fighter = {
    cardsFor(series).find(_.name == name).getOrElse(cardsFor(series).head)
  }

  val dataDir = new File(System.getProperty("user.home"), ".bixuu_anime_fighting_cards_3d")
  var frame: JFrame = _
  var currentAccount: PlayerAccount = _
  var currentSeries = "Naruto Shippuden"
  var currentDifficulty = "MEDIUM"
  var currentLevel = 1
  var playerCard: Fighter = _
  var enemyCard: Fighter = _
  var selected = false
  var battleReady = false
  var battleResolved = false
  var isLanBattle = false
  var currentOpponentName = "ROBOT ENEMY"
  var playerView: NinjaCardView = _
  var enemyView: NinjaCardView = _
  var messageLabel: JLabel = _
  var scoreLabel: JLabel = _
  var statPanel: JPanel = _
  var actionPanel: JPanel = _
  var rootPanel: JPanel = _
  var statButtons = Vector.empty[JButton]
  @volatile var animTick = 0L

  def sha256(value: String): String = {
    val bytes = MessageDigest.getInstance("SHA-256").digest(value.getBytes("UTF-8"))
    bytes.map(b => "%02x".format(b & 0xff)).mkString
  }
  def accountFile(username: String): File = new File(dataDir, sha256(username.trim.toLowerCase(Locale.ROOT)) + ".properties")
  def encodeText(value: String): String = Base64.getEncoder.encodeToString(value.getBytes("UTF-8"))
  def decodeText(value: String): String = new String(Base64.getDecoder.decode(value), "UTF-8")

  def saveAccount(a: PlayerAccount): Unit = {
    dataDir.mkdirs()
    val p = new Properties()
    p.setProperty("username", a.username)
    p.setProperty("passwordHash", a.passwordHash)
    p.setProperty("wins", a.wins.toString)
    p.setProperty("losses", a.losses.toString)
    p.setProperty("battles", a.battles.toString)
    p.setProperty("cards", a.cards.map(encodeText).mkString(","))
    val out = new FileOutputStream(accountFile(a.username))
    try p.store(out, "Bixuu Anime Fighting Cards 3D") finally out.close()
  }
  def loadAccount(username: String): PlayerAccount = {
    val file = accountFile(username)
    if (!file.isFile) return null
    val p = new Properties()
    val in = new FileInputStream(file)
    try p.load(in) finally in.close()
    val savedName = p.getProperty("username", "")
    val savedHash = p.getProperty("passwordHash", "")
    if (savedName.isEmpty || savedHash.isEmpty) return null
    val a = new PlayerAccount(savedName, savedHash)
    try a.wins = p.getProperty("wins", "0").toInt catch { case _: Exception => a.wins = 0 }
    try a.losses = p.getProperty("losses", "0").toInt catch { case _: Exception => a.losses = 0 }
    try a.battles = p.getProperty("battles", "0").toInt catch { case _: Exception => a.battles = 0 }
    val raw = p.getProperty("cards", "")
    if (raw.nonEmpty) raw.split(",").filter(_.nonEmpty).foreach { item =>
      try a.cards += decodeText(item) catch { case _: Exception => () }
    }
    a
  }

  def label(text: String, size: Int, color: Color): JLabel = {
    val l = new JLabel(text, SwingConstants.CENTER)
    l.setFont(new Font("Segoe UI", Font.BOLD, size)); l.setForeground(color); l
  }
  def title(text: String): JLabel = {
    val l = label(text, 25, gold)
    l.setBorder(BorderFactory.createEmptyBorder(14, 6, 14, 6)); l
  }
  def button(text: String): JButton = {
    val b = new JButton(text)
    b.setFont(new Font("Segoe UI", Font.BOLD, 13))
    b.setForeground(Color.WHITE); b.setBackground(new Color(32, 35, 72))
    b.setFocusPainted(false); b.setBorder(BorderFactory.createCompoundBorder(
      BorderFactory.createLineBorder(new Color(125, 110, 255), 1),
      BorderFactory.createEmptyBorder(8, 12, 8, 12)))
    b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
    b.addMouseListener(new MouseAdapter {
      override def mouseEntered(e: MouseEvent): Unit = { b.setBackground(new Color(65, 48, 125)); b.repaint() }
      override def mouseExited(e: MouseEvent): Unit = { b.setBackground(new Color(32, 35, 72)); b.repaint() }
    })
    b
  }
  def plainPanel(layout: LayoutManager): JPanel = {
    val p = new JPanel(layout); p.setOpaque(false); p
  }
  class CosmicPanel(layout: LayoutManager) extends JPanel(layout) {
    setOpaque(true); setBackground(bg)
    override def paintComponent(graphics: Graphics): Unit = {
      val g = graphics.create().asInstanceOf[Graphics2D]
      try {
        val w = getWidth; val h = getHeight
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        g.setPaint(new GradientPaint(0, 0, new Color(8, 10, 27), w, h, new Color(32, 15, 66)))
        g.fillRect(0, 0, w, h)
        val pulse = (Math.sin(animTick / 18.0) * 0.5 + 0.5)
        g.setColor(new Color(45, 75, 190, (15 + pulse * 24).toInt))
        g.fillOval(w / 2 - w / 3, h / 2 - h / 3, w * 2 / 3, h * 2 / 3)
        var i = 0
        while (i < 85) {
          val x = Math.floorMod(i * 131 + (animTick * (i % 3 + 1) / 11).toInt, math.max(1, w))
          val y = Math.floorMod(i * 79 + (i % 5) * 21, math.max(1, h))
          val a = 75 + Math.floorMod(i * 17 + animTick.toInt, 150)
          g.setColor(new Color(170, 195, 255, a))
          g.fillOval(x, y, if (i % 8 == 0) 3 else 2, if (i % 8 == 0) 3 else 2)
          i += 1
        }
        g.setColor(new Color(90, 65, 210, 30))
        for (x <- 0 until w by 48) g.drawLine(x, 0, x - 90, h)
      } finally g.dispose()
    }
  }

  def screen(p: JPanel, width: Int, height: Int): Unit = {
    frame.setContentPane(p); frame.setSize(width, height); frame.setLocationRelativeTo(null)
    frame.revalidate(); frame.repaint(); frame.setVisible(true)
  }
  def showMessage(text: String, caption: String = "BIXUU", kind: Int = JOptionPane.INFORMATION_MESSAGE): Unit =
    JOptionPane.showMessageDialog(frame, text, caption, kind)

  def showLogin(): Unit = {
    val page = new CosmicPanel(new BorderLayout(12, 12))
    page.setBorder(new EmptyBorder(22, 28, 22, 28))
    page.add(title("BIXUU 3D ANIME CARD BATTLES"), BorderLayout.NORTH)
    val form = new JPanel(new GridLayout(4, 1, 9, 9)); form.setOpaque(false)
    form.setBorder(new EmptyBorder(25, 120, 20, 120))
    val username = new JTextField(); val password = new JPasswordField()
    username.setFont(new Font("Segoe UI", Font.PLAIN, 18)); password.setFont(new Font("Segoe UI", Font.PLAIN, 18))
    form.add(label("PLAYER ID / USERNAME", 14, textColor)); form.add(username)
    form.add(label("PASSWORD", 14, textColor)); form.add(password)
    page.add(form, BorderLayout.CENTER)
    val footer = plainPanel(new BorderLayout(8, 8))
    footer.add(label("Create an account or log in to continue.", 13, Color.LIGHT_GRAY), BorderLayout.NORTH)
    val buttons = new JPanel(new GridLayout(1, 3, 10, 10)); buttons.setOpaque(false)
    val login = button("LOGIN"); val create = button("CREATE ACCOUNT"); val restore = button("RESTORE BACKUP")
    login.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = {
      val name = username.getText.trim; val pass = new String(password.getPassword)
      if (name.isEmpty || pass.isEmpty) showMessage("Enter your ID and password.", "LOGIN", JOptionPane.WARNING_MESSAGE)
      else try {
        val a = loadAccount(name)
        if (a == null) showMessage("Account not found. Create it first.", "LOGIN FAILED", JOptionPane.ERROR_MESSAGE)
        else if (a.passwordHash != sha256(pass)) showMessage("Incorrect password.", "LOGIN FAILED", JOptionPane.ERROR_MESSAGE)
        else { currentAccount = a; showHome() }
      } catch { case ex: Exception => showMessage("Login error: " + ex.getMessage, "ERROR", JOptionPane.ERROR_MESSAGE) }
    }})
    create.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = {
      val name = username.getText.trim; val pass = new String(password.getPassword)
      if (!name.matches("[A-Za-z0-9_. -]{3,24}")) showMessage("Use 3-24 letters, numbers, spaces, dot, dash or underscore.", "INVALID ID", JOptionPane.WARNING_MESSAGE)
      else if (pass.length < 4) showMessage("Password must contain at least 4 characters.", "PASSWORD", JOptionPane.WARNING_MESSAGE)
      else if (accountFile(name).exists()) showMessage("This ID already exists. Log in instead.", "ACCOUNT EXISTS", JOptionPane.WARNING_MESSAGE)
      else try { currentAccount = new PlayerAccount(name, sha256(pass)); saveAccount(currentAccount); showHome() }
      catch { case ex: Exception => showMessage("Could not save account: " + ex.getMessage, "SAVE ERROR", JOptionPane.ERROR_MESSAGE) }
    }})
    restore.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = restoreBackup() })
    buttons.add(login); buttons.add(create); buttons.add(restore); footer.add(buttons, BorderLayout.CENTER)
    page.add(footer, BorderLayout.SOUTH); screen(page, 850, 610)
  }

  def showHome(): Unit = {
    val page = new CosmicPanel(new BorderLayout(12, 12)); page.setBorder(new EmptyBorder(16, 22, 18, 22))
    page.add(title("THE PLAY BATTLE OF CARDS - 3D EDITION"), BorderLayout.NORTH)
    page.add(label("WELCOME, " + currentAccount.username.toUpperCase(Locale.ROOT), 17, textColor), BorderLayout.CENTER)
    val menu = new JPanel(new GridLayout(5, 2, 12, 12)); menu.setOpaque(false)
    menu.setBorder(new EmptyBorder(15, 55, 20, 55))
    val anime = button("ANIME FIGHTING CARDS"); val pokemon = button("POKEMON CARDS")
    val actionArena = button("3D ACTION ARENA - 1V1 / 3V1 / 3V3")
    val multiplayer = button("MULTIPLAYER - AUTO IP / PORT"); val uno = button("UNO CARD GAME")
    val profile = button("MY PROFILE"); val collection = button("CARD COLLECTION")
    val backup = button("EXPORT ACCOUNT BACKUP"); val logout = button("LOG OUT")
    anime.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showSeriesMenu() })
    actionArena.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showActionArenaMenu() })
    pokemon.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showDifficulty("Pokemon") })
    multiplayer.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showRoomBrowser() })
    uno.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = startUno() })
    profile.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showProfile() })
    collection.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showCollection() })
    backup.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = exportBackup() })
    logout.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { closePeer(); currentAccount = null; showLogin() } })
    Vector(anime,pokemon,actionArena,multiplayer,uno,profile,collection,backup,logout).foreach(b => menu.add(b))
    page.add(menu, BorderLayout.SOUTH); screen(page, 950, 710)
  }

  def showSeriesMenu(): Unit = {
    val page = new CosmicPanel(new BorderLayout(10, 10)); page.add(title("CHOOSE YOUR ANIME UNIVERSE"), BorderLayout.NORTH)
    val grid = new JPanel(new GridLayout(4, 1, 12, 12)); grid.setOpaque(false); grid.setBorder(new EmptyBorder(25, 100, 25, 100))
    val names = Vector("NARUTO SHIPPUDEN", "DEMON SLAYER", "JUJUTSU KAISEN", "BACK TO HOME")
    names.zipWithIndex.foreach { case (name, i) =>
      val b = button(name); b.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = {
        if (i == 3) showHome() else showDifficulty(seriesNames(i))
      }}); grid.add(b)
    }
    page.add(grid, BorderLayout.CENTER); screen(page, 800, 620)
  }

  def showDifficulty(series: String): Unit = {
    val page = new CosmicPanel(new BorderLayout(10, 10)); page.add(title(series.toUpperCase(Locale.ROOT) + " - SELECT DIFFICULTY"), BorderLayout.NORTH)
    val options = new JPanel(new GridLayout(4, 1, 12, 12)); options.setOpaque(false); options.setBorder(new EmptyBorder(25, 110, 25, 110))
    Vector("EASY", "MEDIUM", "HARD", "BACK").foreach { d =>
      val b = button(if (d == "BACK") d else d + " - 30 LEVELS")
      b.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = {
        if (d == "BACK") { if (series == "Pokemon") showHome() else showSeriesMenu() }
        else startCampaign(series, d)
      }}); options.add(b)
    }
    page.add(options, BorderLayout.CENTER); screen(page, 800, 620)
  }

  def startCampaign(series: String, difficulty: String): Unit = {
    closePeer(); isLanBattle = false; currentSeries = series; currentDifficulty = difficulty; currentLevel = 1; showBattle()
  }
  def statValue(f: Fighter, stat: String): Int = stat match {
    case "POWER" => f.power; case "SPEED" => f.speed; case "DEFENSE" => f.defense; case "CHAKRA" => f.chakra; case _ => 0
  }
  def robotStat(f: Fighter, stat: String): Int = {
    val bonus = if (currentDifficulty == "EASY") -7 else if (currentDifficulty == "HARD") 8 else 0
    math.max(1, math.min(100, statValue(f, stat) + bonus + (currentLevel - 1) / 5))
  }

  class NinjaCardView(val fighter: Fighter, val isPlayerCard: Boolean) extends JPanel {
    var revealed = false
    var chosen = false
    var flipScale = 1.0
    setPreferredSize(new Dimension(280, 345)); setMinimumSize(new Dimension(190, 250)); setOpaque(false)
    setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
    addMouseListener(new MouseAdapter { override def mouseClicked(e: MouseEvent): Unit = {
      if (isPlayerCard && revealed && battleReady && !battleResolved) {
        selected = true; chosen = true
        if (messageLabel != null) messageLabel.setText("CARD SELECTED - CHOOSE A STAT BELOW!")
        updateBattleControls(); repaint()
      }
    }})
    override def paintComponent(graphics: Graphics): Unit = {
      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)
        val w = getWidth; val h = getHeight
        val cw = math.min(w - 26, 270); val ch = math.min(h - 18, 330)
        val x = (w - cw) / 2; val y = (h - ch) / 2
        g.setColor(new Color(0, 0, 0, 115)); g.fillRoundRect(x + 7, y + 11, cw, ch, 28, 28)
        g.translate(w / 2.0, 0.0); g.scale(math.max(0.055, flipScale), 1.0); g.translate(-w / 2.0, 0.0)
        if (!revealed) {
          g.setPaint(new GradientPaint(x, y, new Color(45, 30, 105), x + cw, y + ch, new Color(12, 15, 48)))
          g.fillRoundRect(x, y, cw, ch, 28, 28)
          g.setPaint(new GradientPaint(x, y, gold, x + cw, y + ch, purple)); g.setStroke(new BasicStroke(4))
          g.drawRoundRect(x + 5, y + 5, cw - 10, ch - 10, 24, 24)
          g.setColor(new Color(255, 255, 255, 35)); g.setStroke(new BasicStroke(1.2f))
          var r = 0
          while (r < 7) { g.drawOval(x + 30 + r * 8, y + 62 + r * 8, cw - 60 - r * 16, cw - 60 - r * 16); r += 1 }
          g.setColor(gold); g.setFont(new Font("Segoe UI", Font.BOLD, 29)); drawCentered(g, "BIXUU", x, cw, y + ch / 2 - 4)
          g.setFont(new Font("Segoe UI", Font.BOLD, 12)); drawCentered(g, "LEGENDARY CARD", x, cw, y + ch / 2 + 26)
          g.setColor(new Color(170, 200, 255, 160)); g.fillOval(x + cw / 2 - 5, y + 65, 10, 10)
        } else {
          val glow = (Math.sin(animTick / 9.0) * 0.5 + 0.5)
          g.setPaint(new GradientPaint(x, y, fighter.color.brighter(), x + cw, y + ch, new Color(11, 15, 32)))
          g.fillRoundRect(x, y, cw, ch, 28, 28)
          g.setPaint(new GradientPaint(x, y, new Color(8, 12, 28, 225), x + cw, y + ch, new Color(22, 22, 54, 235)))
          g.fillRoundRect(x + 8, y + 8, cw - 16, ch - 16, 22, 22)
          g.setColor(new Color(fighter.color.getRed, fighter.color.getGreen, fighter.color.getBlue, 155))
          g.setStroke(new BasicStroke(3.0f)); g.drawRoundRect(x + 4, y + 4, cw - 8, ch - 8, 25, 25)
          g.setColor(new Color(255, 210, 90, (65 + glow * 120).toInt)); g.setStroke(new BasicStroke(1.5f))
          g.drawRoundRect(x + 11, y + 11, cw - 22, ch - 22, 20, 20)
          g.setFont(new Font("Segoe UI", Font.BOLD, if (fighter.name.length > 15) 15 else 18))
          g.setColor(Color.WHITE); drawCentered(g, fighter.name, x, cw, y + 34)
          g.setFont(new Font("Segoe UI", Font.BOLD, 11)); g.setColor(fighter.color.brighter())
          val family = if (currentSeries == "Pokemon") "POKEMON" else currentSeries.toUpperCase(Locale.ROOT)
          drawCentered(g, family, x, cw, y + 55)
          val cx = x + cw / 2; val artY = y + 72; val artH = math.max(75, ch / 3)
          g.setColor(new Color(fighter.color.getRed, fighter.color.getGreen, fighter.color.getBlue, 45))
          g.fillOval(cx - 62, artY - 4, 124, 124)
          g.setColor(new Color(255, 255, 255, 25)); g.fillOval(cx - 54, artY + 2, 108, 108)
          g.setPaint(new GradientPaint(cx - 38, artY + 8, fighter.color.brighter(), cx + 35, artY + artH, fighter.color.darker()))
          g.fillOval(cx - 33, artY + 10, 66, 68)
          g.fillRoundRect(cx - 48, artY + 70, 96, 54, 28, 20)
          g.setColor(new Color(255, 255, 255, 220)); g.fillOval(cx - 19, artY + 34, 9, 10); g.fillOval(cx + 10, artY + 34, 9, 10)
          g.setColor(new Color(20, 20, 35)); g.fillOval(cx - 16, artY + 37, 4, 5); g.fillOval(cx + 13, artY + 37, 4, 5)
          g.setColor(Color.WHITE); g.setFont(new Font("Segoe UI", Font.BOLD, 12))
          val baseY = y + ch - 91
          drawStat(g, x + 21, baseY, "POWER", fighter.power, cw - 42)
          drawStat(g, x + 21, baseY + 21, "SPEED", fighter.speed, cw - 42)
          drawStat(g, x + 21, baseY + 42, "DEFENSE", fighter.defense, cw - 42)
          drawStat(g, x + 21, baseY + 63, "CHAKRA", fighter.chakra, cw - 42)
          g.setFont(new Font("Segoe UI", Font.BOLD, 10)); g.setColor(new Color(255, 220, 125))
          drawCentered(g, fighter.special, x, cw, y + ch - 13)
          if (isPlayerCard && chosen) {
            g.setColor(new Color(75, 255, 180, 220)); g.setStroke(new BasicStroke(4)); g.drawRoundRect(x + 2, y + 2, cw - 4, ch - 4, 26, 26)
            g.setFont(new Font("Segoe UI", Font.BOLD, 12)); drawCentered(g, "SELECTED!", x, cw, y + ch - 30)
          }
        }
      } finally g.dispose()
    }
    private def drawCentered(g: Graphics2D, s: String, x: Int, w: Int, y: Int): Unit = {
      val m = g.getFontMetrics; g.drawString(s, x + (w - m.stringWidth(s)) / 2, y)
    }
    private def drawStat(g: Graphics2D, x: Int, y: Int, name: String, value: Int, width: Int): Unit = {
      g.setColor(new Color(230, 238, 255)); g.drawString(name, x, y + 10)
      val bx = x + 66; val bw = math.max(30, width - 102)
      g.setColor(new Color(45, 48, 75)); g.fillRoundRect(bx, y, bw, 8, 8, 8)
      g.setPaint(new GradientPaint(bx, y, fighter.color.brighter(), bx + bw, y + 8, fighter.color.darker()))
      g.fillRoundRect(bx, y, bw * value / 100, 8, 8, 8)
      g.setColor(gold); g.drawString(value.toString, bx + bw + 8, y + 10)
    }
  }

  class ArenaPanel extends JPanel(new GridLayout(1, 2, 16, 8)) {
    setOpaque(false)
    override def paintComponent(graphics: Graphics): Unit = {
      val g = graphics.create().asInstanceOf[Graphics2D]
      try {
        val w = getWidth; val h = getHeight
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        g.setPaint(new GradientPaint(0, 0, new Color(14, 18, 52, 225), w, h, new Color(31, 12, 55, 230)))
        g.fillRoundRect(0, 0, w, h, 34, 34)
        val pulse = (Math.sin(animTick / 8.0) * 0.5 + 0.5)
        g.setColor(new Color(80, 95, 255, (18 + pulse * 28).toInt))
        g.fillOval(w / 2 - 110, h / 2 - 115, 220, 220)
        var i = 0
        while (i < 26) {
          val xx = Math.floorMod(i * 83 + (animTick * (1 + i % 4) / 4).toInt, math.max(1, w))
          val yy = Math.floorMod(i * 59 + (i % 7) * 13, math.max(1, h))
          g.setColor(new Color(120, 185, 255, 70 + (i % 4) * 30))
          g.fillOval(xx, yy, 3 + i % 3, 3 + i % 3); i += 1
        }
        g.setColor(new Color(255, 210, 100, 85)); g.setStroke(new BasicStroke(2f))
        g.drawRoundRect(2, 2, w - 4, h - 4, 32, 32)
      } finally g.dispose()
    }
  }

  def flipCard(view: NinjaCardView, done: () => Unit): Unit = {
    var step = 0
    val total = 14
    val timer = new Timer(24, new ActionListener { override def actionPerformed(e: ActionEvent): Unit = {
      step += 1
      val angle = Math.PI * step / total
      view.flipScale = math.max(0.045, Math.abs(Math.cos(angle)))
      if (step == 7) view.revealed = true
      view.repaint()
      if (step >= total) { val t = e.getSource.asInstanceOf[Timer]; t.stop(); view.flipScale = 1.0; view.repaint(); done() }
    }})
    timer.start()
  }

  def showBattle(): Unit = {
    selected = false; battleReady = false; battleResolved = false
    if (!isLanBattle) { playerCard = randomFighter(currentSeries); enemyCard = randomFighter(currentSeries); while (enemyCard.name == playerCard.name) enemyCard = randomFighter(currentSeries); currentOpponentName = "ROBOT ENEMY" }
    val page = new CosmicPanel(new BorderLayout(10, 10)); page.setBorder(new EmptyBorder(10, 14, 10, 14))
    page.add(title(currentSeries.toUpperCase(Locale.ROOT) + "  |  LEVEL " + currentLevel + "/30" + (if (isLanBattle) "  |  LAN DUEL" else "")), BorderLayout.NORTH)
    val arena = new ArenaPanel()
    val left = plainPanel(new BorderLayout(4, 4)); left.add(label("YOUR CARD", 14, textColor), BorderLayout.NORTH)
    playerView = new NinjaCardView(playerCard, true); left.add(playerView, BorderLayout.CENTER)
    val right = plainPanel(new BorderLayout(4, 4)); right.add(label(currentOpponentName, 14, textColor), BorderLayout.NORTH)
    enemyView = new NinjaCardView(enemyCard, false); right.add(enemyView, BorderLayout.CENTER)
    arena.add(left); arena.add(right); page.add(arena, BorderLayout.CENTER)
    actionPanel = plainPanel(new BorderLayout(7, 7))
    messageLabel = label("CARD REVEAL INCOMING...", 15, gold)
    actionPanel.add(messageLabel, BorderLayout.NORTH)
    statPanel = new JPanel(new GridLayout(1, 4, 8, 8)); statPanel.setOpaque(false)
    statButtons = stats.map { stat =>
      val b = button("CHOOSE " + stat); b.setEnabled(false)
      b.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = fight(stat) })
      statPanel.add(b); b
    }
    actionPanel.add(statPanel, BorderLayout.CENTER)
    val footer = plainPanel(new BorderLayout(5, 5))
    scoreLabel = label("WINS: " + currentAccount.wins + "  |  LOSSES: " + currentAccount.losses + "  |  CARDS: " + currentAccount.cards.size, 12, textColor)
    footer.add(scoreLabel, BorderLayout.CENTER)
    val back = button("BACK TO HOME")
    back.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { closePeer(); isLanBattle = false; showHome() } })
    footer.add(back, BorderLayout.EAST); actionPanel.add(footer, BorderLayout.SOUTH)
    page.add(actionPanel, BorderLayout.SOUTH); screen(page, 1050, 820)
    flipCard(playerView, () => flipCard(enemyView, () => { battleReady = true; messageLabel.setText("CARDS REVEALED! TAP YOUR CARD, THEN CHOOSE A STAT."); updateBattleControls() }))
  }
  def updateBattleControls(): Unit = {
    statButtons.foreach(_.setEnabled(battleReady && selected && !battleResolved))
    if (playerView != null) playerView.repaint()
  }

  def fight(stat: String): Unit = {
    if (!battleReady || !selected || battleResolved) return
    if (isLanBattle && peer != null) { fightLan(stat); return }
    battleResolved = true; updateBattleControls()
    val mine = statValue(playerCard, stat); val theirs = robotStat(enemyCard, stat)
    currentAccount.battles += 1
    if (mine > theirs) {
      currentAccount.wins += 1; currentAccount.cards += (currentSeries + "|" + enemyCard.name); saveAccount(currentAccount)
      showResult("YOU WIN!", stat + ": " + mine + " vs " + theirs, "NEW CARD UNLOCKED: " + enemyCard.name, true, false)
    } else if (mine < theirs) {
      currentAccount.losses += 1; saveAccount(currentAccount)
      showResult("ROBOT WINS!", stat + ": " + mine + " vs " + theirs, "Try again in the next battle!", false, false)
    } else {
      saveAccount(currentAccount); showResult("DRAW!", stat + ": " + mine + " vs " + theirs, "Both cards have equal stats.", false, false)
    }
  }
  def showResult(result: String, detail: String, reward: String, won: Boolean, multiplayerResult: Boolean): Unit = {
    scoreLabel.setText("WINS: " + currentAccount.wins + "  |  LOSSES: " + currentAccount.losses + "  |  CARDS: " + currentAccount.cards.size)
    messageLabel.setText("<html><center><b>" + result + "</b><br>" + detail + "<br>" + reward + "</center></html>")
    statButtons.foreach(_.setEnabled(false))
    val resultPanel = new JPanel(new GridLayout(2, 1, 6, 6)); resultPanel.setOpaque(false)
    resultPanel.add(label(result, 21, if (won) new Color(100, 255, 145) else new Color(255, 180, 100)))
    resultPanel.add(label(reward, 13, textColor))
    val next = if (multiplayerResult) button("RETURN TO HOME") else if (currentLevel >= 30) button("FINISH CAMPAIGN") else button("NEXT LEVEL")
    next.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = {
      if (multiplayerResult) { closePeer(); isLanBattle = false; showHome() }
      else if (currentLevel >= 30) { showMessage("You completed all 30 levels!\nWins: " + currentAccount.wins + "\nCards: " + currentAccount.cards.size, "CAMPAIGN COMPLETE"); showHome() }
      else { currentLevel += 1; showBattle() }
    }})
    resultPanel.add(next)
    actionPanel.remove(statPanel); actionPanel.add(resultPanel, BorderLayout.CENTER)
    actionPanel.revalidate(); actionPanel.repaint(); rootPanel = frame.getContentPane.asInstanceOf[JPanel]; rootPanel.revalidate(); rootPanel.repaint()
  }

  def collectionText: String = {
    if (currentAccount.cards.isEmpty) "YOUR COLLECTION IS EMPTY.\n\nWin a battle to unlock a card."
    else {
      val grouped = currentAccount.cards.groupBy((s: String) => s).toSeq.sortBy(_._1)
      val lines = grouped.map { case (key, copies) =>
        val parts = key.split("\\|", 2); val series = if (parts.length > 1) parts(0) else "Unknown"; val name = if (parts.length > 1) parts(1) else key
        series + " - " + name + " x" + copies.size
      }
      "MY EARNED CARDS\n\n" + lines.mkString("\n") + "\n\nTOTAL CARDS: " + currentAccount.cards.size + "\nUNIQUE CARDS: " + grouped.size
    }
  }
  def showCollection(): Unit = showTextPage("MY CARD COLLECTION", collectionText)
  def showProfile(): Unit = {
    val a = currentAccount
    showTextPage("MY PROFILE", "PLAYER PROFILE\n\nPlayer ID: " + a.username + "\n\nBattles: " + a.battles + "\nWins: " + a.wins + "\nLosses: " + a.losses + "\nCards collected: " + a.cards.size + "\n\nSave folder:\n" + dataDir.getAbsolutePath)
  }
  def showTextPage(pageTitle: String, body: String): Unit = {
    val page = new CosmicPanel(new BorderLayout(10, 10)); page.setBorder(new EmptyBorder(14, 18, 14, 18)); page.add(title(pageTitle), BorderLayout.NORTH)
    val area = new JTextArea(body); area.setEditable(false); area.setLineWrap(true); area.setWrapStyleWord(true)
    area.setFont(new Font("Consolas", Font.PLAIN, 15)); area.setForeground(textColor); area.setBackground(panelColor); area.setBorder(new EmptyBorder(14, 14, 14, 14))
    page.add(new JScrollPane(area), BorderLayout.CENTER)
    val back = button("BACK TO HOME"); back.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showHome() })
    val footer = plainPanel(new FlowLayout(FlowLayout.CENTER)); footer.add(back); page.add(footer, BorderLayout.SOUTH); screen(page, 820, 700)
  }
  def exportBackup(): Unit = {
    if (currentAccount == null) return
    try {
      saveAccount(currentAccount); val chooser = new JFileChooser(); chooser.setDialogTitle("Export Account Backup")
      chooser.setSelectedFile(new File(currentAccount.username + ".bixuu"))
      if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
        val in = accountFile(currentAccount.username).toPath; val out = chooser.getSelectedFile.toPath
        java.nio.file.Files.copy(in, out, java.nio.file.StandardCopyOption.REPLACE_EXISTING)
        showMessage("Account backup exported.", "BACKUP")
      }
    } catch { case ex: Exception => showMessage("Backup failed: " + ex.getMessage, "ERROR", JOptionPane.ERROR_MESSAGE) }
  }
  def restoreBackup(): Unit = {
    val chooser = new JFileChooser(); chooser.setDialogTitle("Restore Bixuu Account Backup")
    if (chooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
      try {
        val p = new Properties(); val in = new FileInputStream(chooser.getSelectedFile)
        try p.load(in) finally in.close()
        val username = p.getProperty("username", "").trim
        val hash = p.getProperty("passwordHash", "")
        if (username.isEmpty || hash.isEmpty) throw new IOException("This is not a valid Bixuu account backup.")
        dataDir.mkdirs(); val target = accountFile(username)
        if (target.exists()) {
          val choice = JOptionPane.showConfirmDialog(frame, "This account already exists. Replace it with the backup?", "RESTORE ACCOUNT", JOptionPane.YES_NO_OPTION)
          if (choice != JOptionPane.YES_OPTION) return
        }
        java.nio.file.Files.copy(chooser.getSelectedFile.toPath, target.toPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING)
        showMessage("Account restored. Log in with the original username and password.", "RESTORE COMPLETE")
      } catch { case ex: Exception => showMessage("Restore failed: " + ex.getMessage, "RESTORE ERROR", JOptionPane.ERROR_MESSAGE) }
    }
  }

  // ------------------------- AUTOMATIC LAN IP / PORT -------------------------
  val DISCOVERY_PORT = 50678 // fixed UDP beacon port; the TCP game port itself is assigned dynamically by the OS
  case class RoomEntry(id: String, name: String, series: String, host: String, port: Int, owner: String, lastSeen: Long) {
    override def toString: String = name + " | " + series + " | " + owner + " | " + host + ":" + port
  }
  class LanPeer(val socket: Socket, val input: DataInputStream, val output: DataOutputStream,
                val remoteUser: String, val series: String)
  @volatile var peer: LanPeer = _
  @volatile var activeServer: ServerSocket = _
  @volatile var activeBeaconSocket: DatagramSocket = _
  @volatile var roomDiscoverySocket: DatagramSocket = _
  @volatile var roomDiscoveryRunning = false
  @volatile var beaconRunning = false
  val discoveredRooms = mutable.LinkedHashMap[String, RoomEntry]()
  var roomModel: DefaultListModel[RoomEntry] = _
  var roomList: JList[RoomEntry] = _
  var roomStatus: JLabel = _

  def netEncode(value: String): String = Base64.getUrlEncoder.withoutPadding().encodeToString(value.getBytes("UTF-8"))
  def netDecode(value: String): String = new String(Base64.getUrlDecoder.decode(value), "UTF-8")
  def localIPAddresses(): String = {
    val found = mutable.ArrayBuffer[String]()
    try {
      val interfaces = NetworkInterface.getNetworkInterfaces
      while (interfaces.hasMoreElements) {
        val ni = interfaces.nextElement()
        if (ni.isUp && !ni.isLoopback) {
          val addresses = ni.getInetAddresses
          while (addresses.hasMoreElements) {
            val a = addresses.nextElement()
            if (a.isInstanceOf[Inet4Address] && !a.isLoopbackAddress && !a.isLinkLocalAddress) found += a.getHostAddress
          }
        }
      }
    } catch { case _: Exception => () }
    if (found.isEmpty) "IP not detected - check Wi-Fi" else found.distinct.mkString(", ")
  }

  def stopRoomDiscovery(): Unit = {
    roomDiscoveryRunning = false
    val ds = roomDiscoverySocket; roomDiscoverySocket = null
    if (ds != null) try ds.close() catch { case _: Exception => () }
  }
  def stopBeacon(): Unit = {
    beaconRunning = false
    val ds = activeBeaconSocket; activeBeaconSocket = null
    if (ds != null) try ds.close() catch { case _: Exception => () }
  }
  def closePeer(): Unit = {
    stopRoomDiscovery(); stopBeacon()
    val p = peer; peer = null
    if (p != null) try p.socket.close() catch { case _: Exception => () }
    val s = activeServer; activeServer = null
    if (s != null) try s.close() catch { case _: Exception => () }
  }
  def startBeacon(port: Int, roomName: String, series: String, owner: String): Unit = {
    stopBeacon()
    try {
      val ds = new DatagramSocket(); ds.setBroadcast(true); activeBeaconSocket = ds; beaconRunning = true
      val t = new Thread(new Runnable { override def run(): Unit = {
        while (beaconRunning && (activeBeaconSocket eq ds)) {
          try {
            val line = "BIXUU3D|" + netEncode(roomName) + "|" + netEncode(series) + "|" + port + "|" + netEncode(owner)
            val bytes = line.getBytes("UTF-8")
            val packet = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("255.255.255.255"), DISCOVERY_PORT)
            ds.send(packet); Thread.sleep(900L)
          } catch { case _: InterruptedException => beaconRunning = false; case _: Exception => Thread.sleep(500L) }
        }
      }})
      t.setDaemon(true); t.setName("bixuu-lan-beacon"); t.start()
    } catch { case ex: Exception => showMessage("Room discovery beacon could not start: " + ex.getMessage, "NETWORK", JOptionPane.WARNING_MESSAGE) }
  }
  def startRoomDiscovery(): Unit = {
    stopRoomDiscovery(); discoveredRooms.synchronized { discoveredRooms.clear() }
    try {
      val ds = new DatagramSocket(null); ds.setReuseAddress(true); ds.bind(new InetSocketAddress(DISCOVERY_PORT)); ds.setSoTimeout(1000)
      roomDiscoverySocket = ds; roomDiscoveryRunning = true
      val t = new Thread(new Runnable { override def run(): Unit = {
        while (roomDiscoveryRunning && (roomDiscoverySocket eq ds)) {
          try {
            val buffer = new Array[Byte](2048); val packet = new DatagramPacket(buffer, buffer.length); ds.receive(packet)
            val line = new String(packet.getData, packet.getOffset, packet.getLength, "UTF-8"); val p = line.split("\\|", -1)
            if (p.length >= 5 && p(0) == "BIXUU3D") {
              val entry = RoomEntry(packet.getAddress.getHostAddress + ":" + p(3), netDecode(p(1)), netDecode(p(2)), packet.getAddress.getHostAddress, p(3).toInt, netDecode(p(4)), System.currentTimeMillis())
              discoveredRooms.synchronized { discoveredRooms(entry.id) = entry }
              SwingUtilities.invokeLater(new Runnable { override def run(): Unit = refreshRoomList() })
            }
          } catch {
            case _: SocketTimeoutException =>
              val cutoff = System.currentTimeMillis() - 5000L
              discoveredRooms.synchronized { discoveredRooms.retain((id, r) => r.lastSeen >= cutoff) }
              SwingUtilities.invokeLater(new Runnable { override def run(): Unit = refreshRoomList() })
            case _: Exception => if (roomDiscoveryRunning) try Thread.sleep(250L) catch { case _: Exception => () }
          }
        }
      }})
      t.setDaemon(true); t.setName("bixuu-room-discovery"); t.start()
    } catch { case ex: Exception => if (roomStatus != null) roomStatus.setText("Auto-scan unavailable. Use JOIN BY IP:PORT. " + ex.getMessage) }
  }
  def refreshRoomList(): Unit = {
    if (roomModel == null || roomList == null) return
    val old = Option(roomList.getSelectedValue).map(_.id).getOrElse("")
    val rooms = discoveredRooms.synchronized { discoveredRooms.values.toVector.sortBy(r => (r.name.toLowerCase(Locale.ROOT), r.host)) }
    roomModel.clear(); rooms.foreach(roomModel.addElement)
    if (old.nonEmpty) { val idx = rooms.indexWhere(_.id == old); if (idx >= 0) roomList.setSelectedIndex(idx) }
    if (roomStatus != null) roomStatus.setText(if (rooms.isEmpty) "Searching local Wi-Fi/LAN for rooms... Use JOIN BY IP:PORT if none appear." else "Found " + rooms.size + " room(s). Select one and press JOIN.")
  }
  def showRoomBrowser(): Unit = {
    stopRoomDiscovery()
    roomModel = new DefaultListModel[RoomEntry](); roomList = new JList[RoomEntry](roomModel)
    roomList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); roomList.setFont(new Font("Segoe UI", Font.PLAIN, 14))
    roomList.setBackground(panelColor); roomList.setForeground(textColor); roomList.setSelectionBackground(new Color(70, 66, 140))
    roomList.addMouseListener(new MouseAdapter { override def mouseClicked(e: MouseEvent): Unit = if (e.getClickCount == 2) joinSelectedRoom() })
    val page = new CosmicPanel(new BorderLayout(10, 10)); page.setBorder(new EmptyBorder(15, 18, 15, 18))
    page.add(title("MULTIPLAYER ROOMS - AUTO IP / AUTO PORT"), BorderLayout.NORTH)
    val center = plainPanel(new BorderLayout(8, 8)); center.add(new JScrollPane(roomList), BorderLayout.CENTER)
    roomStatus = label("Starting room scan...", 12, Color.LIGHT_GRAY); center.add(roomStatus, BorderLayout.SOUTH); page.add(center, BorderLayout.CENTER)
    val controls = new JPanel(new GridLayout(1, 4, 8, 8)); controls.setOpaque(false)
    val create = button("CREATE ROOM"); val join = button("JOIN SELECTED"); val manual = button("JOIN BY IP:PORT"); val back = button("BACK")
    create.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = createLanRoom() })
    join.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = joinSelectedRoom() })
    manual.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = joinByAddress() })
    back.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { stopRoomDiscovery(); showHome() } })
    Vector(create, join, manual, back).foreach(b => controls.add(b)); page.add(controls, BorderLayout.SOUTH)
    screen(page, 990, 650); startRoomDiscovery()
  }
  def createLanRoom(): Unit = {
    closePeer()
    val opts: Array[Object] = seriesNames.map(_.asInstanceOf[Object]).toArray
    val choice = JOptionPane.showInputDialog(frame, "Choose the card series:", "CREATE LAN ROOM", JOptionPane.QUESTION_MESSAGE, null, opts, opts(0))
    if (choice == null) { showRoomBrowser(); return }
    val series = choice.toString
    val defaultName = currentAccount.username + "'s Room"
    val name = JOptionPane.showInputDialog(frame, "Room name:", defaultName)
    if (name == null || name.trim.length < 2) { showRoomBrowser(); return }
    val roomName = name.trim
    var server: ServerSocket = null
    try {
      server = new ServerSocket(0) // OS automatically selects a currently free TCP port
      server.setSoTimeout(0); activeServer = server
      val port = server.getLocalPort; val myCard = randomFighter(series)
      startBeacon(port, roomName, series, currentAccount.username)
      val page = new CosmicPanel(new BorderLayout(10, 10)); page.setBorder(new EmptyBorder(20, 24, 20, 24))
      page.add(title("ROOM CREATED - WAITING FOR YOUR FRIEND"), BorderLayout.NORTH)
      val info = "Room: " + roomName + "\nSeries: " + series + "\n\nDetected local IP address(es):\n" + localIPAddresses() + "\n\nAUTO PORT: " + port + "\n\nYour friend can select this room from the room browser. If automatic discovery is blocked, share one IP and this port as IP:PORT.\n\nAllow this app through Windows Firewall on a private network."
      val area = new JTextArea(info); area.setEditable(false); area.setFont(new Font("Consolas", Font.PLAIN, 15)); area.setForeground(textColor); area.setBackground(panelColor); area.setBorder(new EmptyBorder(18, 18, 18, 18))
      page.add(area, BorderLayout.CENTER)
      val cancel = button("CANCEL ROOM"); cancel.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { closePeer(); showRoomBrowser() } })
      val footer = plainPanel(new FlowLayout(FlowLayout.CENTER)); footer.add(cancel); page.add(footer, BorderLayout.SOUTH); screen(page, 800, 580)
      val serverRef = server
      val t = new Thread(new Runnable { override def run(): Unit = {
        var socket: Socket = null
        try {
          socket = serverRef.accept(); socket.setTcpNoDelay(true)
          val in = new DataInputStream(new BufferedInputStream(socket.getInputStream))
          val out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream))
          val command = in.readUTF(); val remoteName = in.readUTF()
          if (command != "JOIN") throw new IOException("Invalid join request")
          out.writeUTF("WELCOME"); out.writeUTF(series); out.writeUTF(currentAccount.username); out.writeUTF(myCard.name); out.flush()
          val remoteCard = findFighter(series, in.readUTF())
          val connected = new LanPeer(socket, in, out, remoteName, series)
          SwingUtilities.invokeLater(new Runnable { override def run(): Unit = {
            stopBeacon(); activeServer = null; currentSeries = series; currentDifficulty = "MEDIUM"; currentLevel = 1
            isLanBattle = true; currentOpponentName = remoteName; playerCard = myCard; enemyCard = remoteCard; peer = connected; showBattle()
          }})
          try serverRef.close() catch { case _: Exception => () }
        } catch {
          case ex: Exception =>
            if (socket != null) try socket.close() catch { case _: Exception => () }
            SwingUtilities.invokeLater(new Runnable { override def run(): Unit = {
              if (activeServer eq serverRef) { closePeer(); showMessage("Room closed or connection failed: " + ex.getMessage, "LAN ROOM", JOptionPane.WARNING_MESSAGE); showRoomBrowser() }
            }})
        }
      }})
      t.setDaemon(true); t.setName("bixuu-room-host"); t.start()
    } catch { case ex: Exception => showMessage("Could not create room: " + ex.getMessage, "ROOM ERROR", JOptionPane.ERROR_MESSAGE); showRoomBrowser() }
  }
  def joinSelectedRoom(): Unit = {
    if (roomList == null || roomList.getSelectedValue == null) { showMessage("Select a room first or press REFRESH by re-opening the browser.", "JOIN ROOM"); return }
    val r = roomList.getSelectedValue; stopRoomDiscovery(); connectToRoom(r.host, r.port)
  }
  def joinByAddress(): Unit = {
    val raw = JOptionPane.showInputDialog(frame, "Enter host IP:PORT (example 192.168.1.5:52145):", "JOIN BY IP:PORT", JOptionPane.QUESTION_MESSAGE)
    if (raw == null || raw.trim.isEmpty) return
    val parts = raw.trim.split(":", -1)
    if (parts.length != 2) { showMessage("Enter IP:PORT, for example 192.168.1.5:52145", "INVALID ADDRESS", JOptionPane.WARNING_MESSAGE); return }
    val port = try parts(1).toInt catch { case _: Exception => -1 }
    if (port < 1 || port > 65535) { showMessage("Port must be between 1 and 65535.", "INVALID PORT", JOptionPane.WARNING_MESSAGE); return }
    stopRoomDiscovery(); connectToRoom(parts(0).trim, port)
  }
  def connectToRoom(host: String, port: Int): Unit = {
    val waitPage = new CosmicPanel(new BorderLayout()); waitPage.add(title("CONNECTING TO " + host + ":" + port + " ..."), BorderLayout.CENTER); screen(waitPage, 650, 330)
    val t = new Thread(new Runnable { override def run(): Unit = {
      var socket: Socket = null
      try {
        socket = new Socket(); socket.connect(new InetSocketAddress(host, port), 8000); socket.setTcpNoDelay(true)
        val in = new DataInputStream(new BufferedInputStream(socket.getInputStream))
        val out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream))
        out.writeUTF("JOIN"); out.writeUTF(currentAccount.username); out.flush()
        val welcome = in.readUTF(); if (welcome != "WELCOME") throw new IOException("Host rejected the join request")
        val series = in.readUTF(); val hostName = in.readUTF(); val hostCardName = in.readUTF()
        val myCard = randomFighter(series); out.writeUTF(myCard.name); out.flush()
        val connected = new LanPeer(socket, in, out, hostName, series); val remoteCard = findFighter(series, hostCardName)
        SwingUtilities.invokeLater(new Runnable { override def run(): Unit = {
          currentSeries = series; currentDifficulty = "MEDIUM"; currentLevel = 1; isLanBattle = true
          currentOpponentName = hostName; playerCard = myCard; enemyCard = remoteCard; peer = connected; showBattle()
        }})
      } catch {
        case ex: Exception =>
          if (socket != null) try socket.close() catch { case _: Exception => () }
          SwingUtilities.invokeLater(new Runnable { override def run(): Unit = {
            showMessage("Could not join room: " + ex.getMessage + "\nCheck the IP, port, Wi-Fi and Firewall settings.", "JOIN ERROR", JOptionPane.ERROR_MESSAGE)
            showRoomBrowser()
          }})
      }
    }})
    t.setDaemon(true); t.setName("bixuu-room-join"); t.start()
  }
  def fightLan(stat: String): Unit = {
    val connected = peer
    if (connected == null) { showMessage("Multiplayer connection was lost.", "NETWORK", JOptionPane.ERROR_MESSAGE); closePeer(); isLanBattle = false; showHome(); return }
    battleResolved = true; updateBattleControls(); messageLabel.setText("SENDING YOUR MOVE - WAITING FOR FRIEND...")
    val t = new Thread(new Runnable { override def run(): Unit = {
      try {
        connected.output.synchronized { connected.output.writeUTF(stat); connected.output.flush() }
        val otherStat = connected.input.readUTF()
        SwingUtilities.invokeLater(new Runnable { override def run(): Unit = finishLanBattle(stat, otherStat) })
      } catch { case ex: Exception => SwingUtilities.invokeLater(new Runnable { override def run(): Unit = {
        showMessage("Connection lost: " + ex.getMessage, "MULTIPLAYER ERROR", JOptionPane.ERROR_MESSAGE); closePeer(); isLanBattle = false; showHome()
      }}) }
    }})
    t.setDaemon(true); t.setName("bixuu-lan-fight"); t.start()
  }
  def finishLanBattle(myStat: String, otherStat: String): Unit = {
    val mine = statValue(playerCard, myStat); val theirs = statValue(enemyCard, otherStat); currentAccount.battles += 1
    if (mine > theirs) {
      currentAccount.wins += 1; currentAccount.cards += (currentSeries + "|" + enemyCard.name); saveAccount(currentAccount)
      showResult("YOU WIN THE LAN DUEL!", myStat + ": " + mine + " vs friend's " + otherStat + ": " + theirs, "NEW CARD: " + enemyCard.name, true, true)
    } else if (mine < theirs) {
      currentAccount.losses += 1; saveAccount(currentAccount)
      showResult("YOUR FRIEND WINS!", myStat + ": " + mine + " vs friend's " + otherStat + ": " + theirs, "Play again to win a card!", false, true)
    } else { saveAccount(currentAccount); showResult("LAN DUEL DRAW!", myStat + ": " + mine + " vs friend's " + otherStat + ": " + theirs, "Both values match.", false, true) }
  }

  // ================================================================
  // 3D-LOOK ACTION ARENA: LIVE HP COMBAT, 1V1 / 3V1 / 3V3 VS ROBOTS
  // Java2D pseudo-3D rendering, energy effects, AI teammates and KO rules.
  // ================================================================
  class ArenaUnit(
    val fighter: Fighter,
    val side: Int,
    val slot: Int,
    val maxHp: Int,
    var hp: Int,
    var energy: Int,
    var attackAnim: Int,
    var hitFlash: Int,
    var guardTicks: Int,
    var cooldown: Int,
    var aiCooldown: Int,
    var assistCooldown: Int,
    var defeated: Boolean
  )

  case class ArenaProjectile(
    var progress: Double,
    val fromX: Double,
    val fromY: Double,
    val toX: Double,
    val toY: Double,
    val color: Color,
    val target: ArenaUnit,
    val damage: Int,
    val special: Boolean
  )

  var actionArenaMode = "1V1"
  var actionArenaSeries = "Naruto Shippuden"
  var actionArenaHeroName = "Naruto Uzumaki"
  var actionArenaUnits = mutable.ArrayBuffer[ArenaUnit]()
  var actionArenaProjectiles = mutable.ArrayBuffer[ArenaProjectile]()
  var actionArenaActiveSlot = 0
  var actionArenaEnded = false
  var actionArenaWon = false
  var actionArenaMessage = "FIGHT!"
  var actionArenaCanvas: ActionArenaCanvas = _
  var actionArenaStatus: JLabel = _
  var actionArenaControls: JPanel = _
  var actionArenaAttackButton: JButton = _
  var actionArenaSpecialButton: JButton = _
  var actionArenaGuardButton: JButton = _
  var actionArenaTimer: Timer = _

  def showActionArenaMenu(): Unit = {
    val page = new CosmicPanel(new BorderLayout(12, 12))
    page.setBorder(new EmptyBorder(18, 24, 18, 24))
    page.add(title("BIXUU ACTION ARENA - CHOOSE BATTLE MODE"), BorderLayout.NORTH)
    val options = new JPanel(new GridLayout(5, 1, 10, 10))
    options.setOpaque(false)
    options.setBorder(new EmptyBorder(18, 105, 18, 105))
    val duel = button("1V1 - HERO VS ROBOT")
    val boss = button("3V1 - THREE HEROES VS BOSS")
    val squad = button("3V3 - SQUAD WAR VS ROBOT TEAM")
    val online = button("OPEN ONLINE LAN ROOM BROWSER")
    val back = button("BACK TO HOME")
    duel.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showActionSeriesChooser("1V1") })
    boss.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showActionSeriesChooser("3V1") })
    squad.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showActionSeriesChooser("3V3") })
    online.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showRoomBrowser() })
    back.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showHome() })
    Vector(duel, boss, squad, online, back).foreach(b => options.add(b))
    page.add(options, BorderLayout.CENTER)
    page.add(label("Robot matches are animated. LAN rooms use the existing network card-duel mode.", 12, Color.LIGHT_GRAY), BorderLayout.SOUTH)
    screen(page, 920, 690)
  }

  def showActionSeriesChooser(mode: String): Unit = {
    val page = new CosmicPanel(new BorderLayout(10, 10))
    page.setBorder(new EmptyBorder(18, 24, 18, 24))
    page.add(title(mode + " ACTION BATTLE - CHOOSE ANIME WORLD"), BorderLayout.NORTH)
    val options = new JPanel(new GridLayout(5, 1, 10, 10))
    options.setOpaque(false)
    options.setBorder(new EmptyBorder(18, 100, 18, 100))
    seriesNames.foreach { series =>
      val b = button(series.toUpperCase(Locale.ROOT))
      b.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showActionHeroChooser(mode, series) })
      options.add(b)
    }
    val back = button("BACK TO MODES")
    back.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showActionArenaMenu() })
    options.add(back)
    page.add(options, BorderLayout.CENTER)
    screen(page, 850, 710)
  }

  def showActionHeroChooser(mode: String, series: String): Unit = {
    val page = new CosmicPanel(new BorderLayout(12, 12))
    page.setBorder(new EmptyBorder(22, 30, 22, 30))
    page.add(title("CHOOSE YOUR MAIN FIGHTER"), BorderLayout.NORTH)
    val fighters = cardsFor(series)
    val choices: Array[Object] = fighters.map(_.name.asInstanceOf[Object]).toArray
    val heroBox = new JComboBox[Object](choices)
    heroBox.setFont(new Font("Segoe UI", Font.BOLD, 17))
    heroBox.setSelectedItem(fighters.head.name)
    val description = label("Your hero fights with attacks, special power, guarding and energy.", 14, textColor)
    val center = new JPanel(new GridLayout(3, 1, 12, 12))
    center.setOpaque(false)
    center.setBorder(new EmptyBorder(45, 110, 45, 110))
    center.add(label("MODE: " + mode + "   |   SERIES: " + series, 15, gold))
    center.add(heroBox)
    center.add(description)
    page.add(center, BorderLayout.CENTER)
    val start = button("ENTER THE 3D ARENA")
    val back = button("BACK")
    val foot = new JPanel(new GridLayout(1, 2, 10, 10)); foot.setOpaque(false)
    start.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = startActionArena(mode, series, heroBox.getSelectedItem.toString) })
    back.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showActionSeriesChooser(mode) })
    foot.add(start); foot.add(back); page.add(foot, BorderLayout.SOUTH)
    screen(page, 850, 580)
  }

  def startActionArena(mode: String, series: String, heroName: String): Unit = {
    closePeer()
    isLanBattle = false
    actionArenaMode = mode
    actionArenaSeries = series
    actionArenaHeroName = heroName
    currentSeries = series
    actionArenaActiveSlot = 0
    actionArenaEnded = false
    actionArenaWon = false
    actionArenaProjectiles.clear()
    actionArenaUnits.clear()
    actionArenaMessage = "FIGHT! ATTACK, USE SPECIAL POWER, AND GUARD."

    val all = cardsFor(series)
    val hero = findFighter(series, heroName)
    val teammates = if (mode == "1V1") Vector.empty[Fighter] else random.shuffle(all.filterNot(_.name == hero.name)).take(2)
    val playerTeam = Vector(hero) ++ teammates
    val enemyCount = if (mode == "3V1") 1 else if (mode == "3V3") 3 else 1
    val enemyPool = random.shuffle(all.filterNot(f => playerTeam.exists(_.name == f.name)))
    val enemyTeam = enemyPool.take(enemyCount)

    playerTeam.zipWithIndex.foreach { case (f, idx) =>
      val hp = 155 + (f.defense * 2 / 5)
      actionArenaUnits += new ArenaUnit(f, 0, idx, hp, hp, 100, 0, 0, 0, 0,
        25 + random.nextInt(25), 32 + random.nextInt(25), false)
    }
    enemyTeam.zipWithIndex.foreach { case (f, idx) =>
      val hp = if (mode == "3V1") 330 + f.defense else 160 + (f.defense * 2 / 5)
      actionArenaUnits += new ArenaUnit(f, 1, idx, hp, hp, 100, 0, 0, 0, 0,
        36 + random.nextInt(28), 36 + random.nextInt(24), false)
    }
    showActionArenaScreen()
  }

  def actionAlive(side: Int): Vector[ArenaUnit] = actionArenaUnits.filter(u => u.side == side && !u.defeated && u.hp > 0).toVector
  def actionActiveHero: ArenaUnit = {
    val team = actionAlive(0)
    team.find(_.slot == actionArenaActiveSlot).getOrElse(team.headOption.orNull)
  }
  def actionUnitX(u: ArenaUnit, w: Int): Double = {
    val teamSize = math.max(1, actionArenaUnits.count(_.side == u.side))
    val center = if (u.side == 0) 0.245 else 0.755
    val direction = if (u.side == 0) 1.0 else -1.0
    val spread = if (teamSize == 1) 0.0 else (u.slot - (teamSize - 1) / 2.0) * 0.105
    (center + direction * spread) * w
  }
  def actionUnitBaseY(u: ArenaUnit, h: Int): Int = {
    val teamSize = math.max(1, actionArenaUnits.count(_.side == u.side))
    (h * 0.75 + (u.slot - (teamSize - 1) / 2.0) * 15).toInt
  }

  class ActionArenaCanvas extends JPanel {
    setOpaque(false)
    setPreferredSize(new Dimension(1060, 520))
    override def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val g = graphics.create().asInstanceOf[Graphics2D]
      try {
        val w = getWidth; val h = getHeight
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
        g.setPaint(new GradientPaint(0, 0, new Color(6, 9, 27), w, h, new Color(35, 12, 67)))
        g.fillRect(0, 0, w, h)

        val pulse = (Math.sin(animTick / 8.0) * 0.5 + 0.5)
        g.setColor(new Color(64, 70, 230, (18 + pulse * 25).toInt))
        g.fillOval(w / 2 - w / 4, h / 8, w / 2, h / 2)
        g.setColor(new Color(100, 125, 255, 65)); g.setStroke(new BasicStroke(2f))
        g.drawOval(w / 2 - 110, h / 5, 220, 160)
        g.drawOval(w / 2 - 145, h / 5 + 15, 290, 130)

        // Perspective arena floor and moving grid.
        val horizon = (h * 0.52).toInt
        val floor = new Polygon(Array(0, w, w, 0), Array(horizon, horizon, h, h), 4)
        g.setPaint(new GradientPaint(0, horizon, new Color(29, 37, 82), 0, h, new Color(7, 9, 25)))
        g.fillPolygon(floor)
        g.setColor(new Color(95, 125, 255, 65)); g.setStroke(new BasicStroke(1f))
        val vanishX = w / 2
        for (i <- 0 to 14) {
          val floorX = i * w / 14
          g.drawLine(vanishX, horizon, floorX, h)
        }
        for (i <- 1 to 9) {
          val f = i.toDouble / 10.0
          val yy = horizon + (h - horizon) * f * f
          g.drawLine(0, yy.toInt, w, yy.toInt)
        }
        g.setColor(new Color(255, 190, 70, 100)); g.setStroke(new BasicStroke(2f))
        g.drawLine(0, horizon, w, horizon)

        g.setFont(new Font("Segoe UI", Font.BOLD, 17))
        g.setColor(new Color(140, 220, 255)); g.drawString("PLAYER SQUAD", 22, 30)
        g.setColor(new Color(255, 135, 135)); val enemyTitle = if (actionArenaMode == "3V1") "BOSS ROBOT" else "ROBOT SQUAD"
        g.drawString(enemyTitle, w - g.getFontMetrics.stringWidth(enemyTitle) - 22, 30)
        g.setFont(new Font("Segoe UI", Font.BOLD, 12)); g.setColor(textColor)
        g.drawString("MODE " + actionArenaMode + "  |  " + actionArenaSeries.toUpperCase(Locale.ROOT), 22, 51)
        g.drawString("TEAM ALIVE: " + actionAlive(0).size, 22, 70)
        val enemyAliveText = "ENEMIES LEFT: " + actionAlive(1).size
        g.drawString(enemyAliveText, w - g.getFontMetrics.stringWidth(enemyAliveText) - 22, 51)

        actionArenaUnits.sortBy(u => actionUnitBaseY(u, h)).foreach(u => drawActionArenaUnit(g, u, w, h))
        actionArenaProjectiles.foreach(fx => drawActionArenaProjectile(g, fx, w, h))

        // Battle message ribbon.
        g.setColor(new Color(5, 8, 20, 205)); g.fillRoundRect(w / 2 - 240, h - 42, 480, 28, 14, 14)
        g.setColor(gold); g.setStroke(new BasicStroke(1.5f)); g.drawRoundRect(w / 2 - 240, h - 42, 480, 28, 14, 14)
        g.setFont(new Font("Segoe UI", Font.BOLD, 12))
        val msgWidth = g.getFontMetrics.stringWidth(actionArenaMessage)
        g.setColor(Color.WHITE); g.drawString(actionArenaMessage, math.max(12, (w - msgWidth) / 2), h - 24)

        if (actionArenaEnded) {
          g.setColor(new Color(2, 4, 15, 190)); g.fillRect(0, 0, w, h)
          g.setFont(new Font("Segoe UI", Font.BOLD, 42))
          val main = if (actionArenaWon) "VICTORY!" else "DEFEAT!"
          g.setColor(if (actionArenaWon) new Color(90, 255, 170) else new Color(255, 115, 125))
          g.drawString(main, (w - g.getFontMetrics.stringWidth(main)) / 2, h / 2 - 12)
          g.setFont(new Font("Segoe UI", Font.BOLD, 17)); g.setColor(Color.WHITE)
          val sub = if (actionArenaWon) "ENEMY TEAM KNOCKED OUT" else "YOUR TEAM WAS KNOCKED OUT"
          g.drawString(sub, (w - g.getFontMetrics.stringWidth(sub)) / 2, h / 2 + 24)
        }
      } finally g.dispose()
    }

    private def drawActionArenaUnit(g: Graphics2D, u: ArenaUnit, w: Int, h: Int): Unit = {
      val baseX = actionUnitX(u, w).toInt
      val baseY = actionUnitBaseY(u, h)
      val scale = 0.80 + (baseY.toDouble / math.max(1, h)) * 0.22
      val bodyH = (150 * scale).toInt
      val bodyW = (76 * scale).toInt
      val bob = if (u.attackAnim > 0) (Math.sin((14 - u.attackAnim) / 14.0 * Math.PI) * 24).toInt * (if (u.side == 0) 1 else -1) else (Math.sin(animTick / 6.0 + u.slot) * 3).toInt
      val cx = baseX + bob
      val topY = baseY - bodyH
      val shade = if (u.hitFlash > 0) Color.WHITE else u.fighter.color
      val pulseAura = (Math.sin(animTick / 6.0 + u.slot * 2) * 0.5 + 0.5)

      // Ground shadow creates depth.
      g.setColor(new Color(0, 0, 0, 120)); g.fillOval(cx - bodyW / 2 - 13, baseY - 7, bodyW + 26, 19)
      g.setColor(new Color(shade.getRed, shade.getGreen, shade.getBlue, (25 + pulseAura * 36).toInt))
      g.fillOval(cx - bodyW, topY - 15, bodyW * 2, bodyH + 25)
      g.setColor(new Color(shade.getRed, shade.getGreen, shade.getBlue, 85)); g.setStroke(new BasicStroke(2f))
      g.drawOval(cx - bodyW - 5, topY + 10, bodyW * 2 + 10, bodyH - 5)

      val old = g.getTransform
      g.translate(cx, 0.0); g.scale(if (u.side == 0) 1.0 else -1.0, 1.0); g.translate(-cx, 0.0)
      try {
        if (u.guardTicks > 0) {
          g.setColor(new Color(95, 205, 255, 65)); g.fillOval(cx - bodyW, topY - 12, bodyW * 2, bodyH + 12)
          g.setColor(new Color(125, 225, 255, 205)); g.setStroke(new BasicStroke(3f)); g.drawOval(cx - bodyW - 4, topY - 16, bodyW * 2 + 8, bodyH + 20)
        }
        // Legs, boots, arms and torso use light/shadow gradients for a pseudo-3D anime silhouette.
        val legY = baseY - (bodyH * 0.33).toInt
        g.setPaint(new GradientPaint(cx - bodyW / 2, legY, shade.brighter(), cx + bodyW / 2, baseY, shade.darker()))
        g.fillRoundRect(cx - (bodyW * 0.36).toInt, legY, (bodyW * 0.28).toInt, (bodyH * 0.34).toInt, 12, 12)
        g.fillRoundRect(cx + (bodyW * 0.08).toInt, legY, (bodyW * 0.28).toInt, (bodyH * 0.34).toInt, 12, 12)
        g.setColor(new Color(20, 25, 42)); g.fillRoundRect(cx - (bodyW * 0.40).toInt, baseY - 13, (bodyW * 0.42).toInt, 13, 7, 7)
        g.fillRoundRect(cx + (bodyW * 0.01).toInt, baseY - 13, (bodyW * 0.42).toInt, 13, 7, 7)

        val torsoY = baseY - (bodyH * 0.62).toInt
        g.setPaint(new GradientPaint(cx - bodyW / 2, torsoY, shade.brighter(), cx + bodyW / 2, baseY, shade.darker()))
        g.fillRoundRect(cx - bodyW / 2, torsoY, bodyW, (bodyH * 0.38).toInt, 20, 20)
        g.setColor(new Color(255, 255, 255, 75)); g.setStroke(new BasicStroke(2f))
        g.drawLine(cx - bodyW / 4, torsoY + 8, cx + bodyW / 5, torsoY + (bodyH * 0.31).toInt)
        g.setColor(shade.darker()); g.fillRoundRect(cx - bodyW / 2 - 10, torsoY + 5, 17, (bodyH * 0.27).toInt, 12, 12)
        g.setPaint(new GradientPaint(cx + bodyW / 2, torsoY, shade.brighter(), cx + bodyW / 2 + 13, torsoY + 45, shade.darker()))
        g.fillRoundRect(cx + bodyW / 2 - 4, torsoY + 5, 18, (bodyH * 0.26).toInt, 12, 12)

        val headW = (40 * scale).toInt
        val headH = (43 * scale).toInt
        val headY = topY + (bodyH * 0.13).toInt
        g.setPaint(new GradientPaint(cx - headW / 2, headY, new Color(255, 220, 185), cx + headW / 2, headY + headH, new Color(166, 107, 95)))
        g.fillOval(cx - headW / 2, headY, headW, headH)
        g.setColor(new Color(25, 20, 42)); g.fillOval(cx - headW / 2 + 2, headY - 5, headW - 4, headH / 2)
        val hair = new Polygon(
          Array(cx - headW / 2, cx - headW / 3, cx - headW / 8, cx + headW / 7, cx + headW / 2, cx + headW / 3, cx - headW / 5),
          Array(headY + 13, headY - 9, headY + 3, headY - 8, headY + 8, headY + 17, headY + 11), 7)
        g.setPaint(new GradientPaint(cx - headW / 2, headY, shade.darker(), cx + headW / 2, headY + 20, shade.brighter()))
        g.fillPolygon(hair)
        g.setColor(Color.WHITE); g.fillOval(cx - (headW * 0.26).toInt, headY + headH / 2, 7, 5)
        g.fillOval(cx + (headW * 0.09).toInt, headY + headH / 2, 7, 5)
        g.setColor(new Color(15, 20, 40)); g.fillOval(cx - (headW * 0.20).toInt, headY + headH / 2 + 1, 3, 4)
        g.fillOval(cx + (headW * 0.15).toInt, headY + headH / 2 + 1, 3, 4)
        g.setColor(new Color(255, 255, 255, 100)); g.setStroke(new BasicStroke(1.5f)); g.drawOval(cx - headW / 2, headY, headW, headH)

        // Signature energy blade/orb near the leading hand.
        if (u.attackAnim > 0) {
          g.setColor(new Color(shade.getRed, shade.getGreen, shade.getBlue, 165)); g.fillOval(cx + bodyW / 2 - 2, torsoY + 5, 37, 37)
          g.setColor(Color.WHITE); g.fillOval(cx + bodyW / 2 + 9, torsoY + 16, 14, 14)
        }
      } finally g.setTransform(old)

      // Name and health/energy bars.
      val name = u.fighter.name + (if (u.slot == actionArenaActiveSlot && u.side == 0 && !actionArenaEnded) "  YOU" else "")
      g.setFont(new Font("Segoe UI", Font.BOLD, 11)); g.setColor(if (u.side == 0) new Color(165, 235, 255) else new Color(255, 175, 180))
      g.drawString(name, cx - g.getFontMetrics.stringWidth(name) / 2, topY - 31)
      val barW = math.max(72, bodyW + 28); val barX = cx - barW / 2; val barY = topY - 24
      g.setColor(new Color(5, 7, 17, 210)); g.fillRoundRect(barX - 2, barY - 2, barW + 4, 11, 8, 8)
      g.setColor(new Color(65, 24, 39)); g.fillRoundRect(barX, barY, barW, 7, 6, 6)
      val hpFraction = math.max(0.0, u.hp.toDouble / math.max(1, u.maxHp))
      val hpColor = if (hpFraction > 0.55) new Color(60, 230, 135) else if (hpFraction > 0.25) new Color(255, 190, 55) else new Color(255, 65, 90)
      g.setPaint(new GradientPaint(barX, barY, hpColor.brighter(), barX + barW, barY + 7, hpColor.darker()))
      g.fillRoundRect(barX, barY, (barW * hpFraction).toInt, 7, 6, 6)
      g.setColor(new Color(50, 80, 135)); g.fillRoundRect(barX, barY + 10, barW, 4, 4, 4)
      g.setColor(new Color(60, 190, 255)); g.fillRoundRect(barX, barY + 10, (barW * u.energy / 100.0).toInt, 4, 4, 4)
      if (u.defeated) {
        g.setFont(new Font("Segoe UI", Font.BOLD, 17)); g.setColor(new Color(255, 95, 110))
        val ko = "KO"; g.drawString(ko, cx - g.getFontMetrics.stringWidth(ko) / 2, topY - 42)
      }
      if (u.hitFlash > 0) { g.setColor(new Color(255, 255, 255, 95)); g.fillOval(cx - bodyW, topY, bodyW * 2, bodyH) }
    }

    private def drawActionArenaProjectile(g: Graphics2D, fx: ArenaProjectile, w: Int, h: Int): Unit = {
      val px = (fx.fromX + (fx.toX - fx.fromX) * fx.progress) * w
      val py = (fx.fromY + (fx.toY - fx.fromY) * fx.progress) * h - Math.sin(fx.progress * Math.PI) * (if (fx.special) 72 else 28)
      val sx = fx.fromX * w; val sy = fx.fromY * h
      g.setColor(new Color(fx.color.getRed, fx.color.getGreen, fx.color.getBlue, if (fx.special) 45 else 35))
      g.setStroke(new BasicStroke(if (fx.special) 19f else 10f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
      g.drawLine(sx.toInt, sy.toInt, px.toInt, py.toInt)
      g.setColor(new Color(fx.color.getRed, fx.color.getGreen, fx.color.getBlue, 215))
      g.setStroke(new BasicStroke(if (fx.special) 7f else 4f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
      g.drawLine(sx.toInt, sy.toInt, px.toInt, py.toInt)
      g.setColor(Color.WHITE); g.fillOval(px.toInt - (if (fx.special) 9 else 5), py.toInt - (if (fx.special) 9 else 5), if (fx.special) 18 else 10, if (fx.special) 18 else 10)
      if (fx.special) {
        g.setColor(new Color(fx.color.getRed, fx.color.getGreen, fx.color.getBlue, 150)); g.setStroke(new BasicStroke(3f))
        g.drawOval(px.toInt - 20, py.toInt - 20, 40, 40)
      }
    }
  }

  def showActionArenaScreen(): Unit = {
    if (actionArenaTimer != null) actionArenaTimer.stop()
    val page = new CosmicPanel(new BorderLayout(8, 8))
    page.setBorder(new EmptyBorder(10, 14, 10, 14))
    page.add(title("BIXUU CINEMATIC ACTION ARENA  |  " + actionArenaMode), BorderLayout.NORTH)
    actionArenaCanvas = new ActionArenaCanvas()
    page.add(actionArenaCanvas, BorderLayout.CENTER)

    val footer = new JPanel(new BorderLayout(6, 6)); footer.setOpaque(false)
    actionArenaStatus = label("Use ATTACK, SPECIAL POWER and GUARD. Teammates assist automatically.", 12, gold)
    footer.add(actionArenaStatus, BorderLayout.NORTH)
    actionArenaControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 7, 5)); actionArenaControls.setOpaque(false)
    actionArenaAttackButton = button("? ATTACK")
    actionArenaSpecialButton = button("? SPECIAL POWER")
    actionArenaGuardButton = button("? GUARD")
    actionArenaAttackButton.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = actionArenaPlayerMove(false) })
    actionArenaSpecialButton.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = actionArenaPlayerMove(true) })
    actionArenaGuardButton.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = actionArenaGuard() })
    actionArenaControls.add(actionArenaAttackButton); actionArenaControls.add(actionArenaSpecialButton); actionArenaControls.add(actionArenaGuardButton)
    val playerCount = actionArenaUnits.count(_.side == 0)
    if (playerCount > 1) {
      for (i <- 0 until playerCount) {
        val slot = i
        val b = button("SWITCH " + (i + 1))
        b.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = switchActionArenaHero(slot) })
        actionArenaControls.add(b)
      }
    }
    val home = button("QUIT TO HOME")
    home.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { if (actionArenaTimer != null) actionArenaTimer.stop(); showHome() } })
    actionArenaControls.add(home)
    footer.add(actionArenaControls, BorderLayout.CENTER)
    page.add(footer, BorderLayout.SOUTH)
    screen(page, 1120, 800)
    refreshActionArenaUI()
    actionArenaTimer = new Timer(40, new ActionListener { override def actionPerformed(e: ActionEvent): Unit = tickActionArena() })
    actionArenaTimer.start()
  }

  def switchActionArenaHero(slot: Int): Unit = {
    if (actionArenaEnded) return
    if (actionArenaUnits.exists(u => u.side == 0 && u.slot == slot && !u.defeated)) {
      actionArenaActiveSlot = slot
      actionArenaMessage = "ACTIVE HERO: " + actionArenaUnits.find(u => u.side == 0 && u.slot == slot).get.fighter.name
      refreshActionArenaUI()
    }
  }

  def actionArenaPlayerMove(special: Boolean): Unit = {
    if (actionArenaEnded) return
    val hero = actionActiveHero
    if (hero == null || hero.defeated) { actionArenaMessage = "Choose a living fighter."; refreshActionArenaUI(); return }
    if (hero.cooldown > 0) { actionArenaMessage = "ATTACK RECHARGING..."; refreshActionArenaUI(); return }
    if (special && hero.energy < 35) { actionArenaMessage = "NOT ENOUGH ENERGY - USE NORMAL ATTACKS TO RECHARGE."; refreshActionArenaUI(); return }
    val targets = actionAlive(1)
    if (targets.isEmpty) return
    val target = targets.minBy(u => (u.hp, u.slot))
    if (special) { hero.energy = math.max(0, hero.energy - 35); hero.cooldown = 30 }
    else { hero.energy = math.min(100, hero.energy + 9); hero.cooldown = 17 }
    launchActionArenaAttack(hero, target, special)
    actionArenaMessage = hero.fighter.name + (if (special) " used " + hero.fighter.special + "!" else " attacks " + target.fighter.name + "!")
    refreshActionArenaUI()
  }

  def actionArenaGuard(): Unit = {
    if (actionArenaEnded) return
    val hero = actionActiveHero
    if (hero == null) return
    hero.guardTicks = 28
    hero.energy = math.min(100, hero.energy + 3)
    actionArenaMessage = hero.fighter.name + " raises an energy guard! Incoming damage is reduced."
    refreshActionArenaUI()
  }

  def launchActionArenaAttack(attacker: ArenaUnit, target: ArenaUnit, special: Boolean): Unit = {
    if (attacker == null || target == null || attacker.defeated || target.defeated) return
    attacker.attackAnim = 14
    val base = if (special) 19 + attacker.fighter.power / 5 + attacker.fighter.chakra / 9 else 7 + attacker.fighter.power / 9 + attacker.fighter.speed / 24
    val rawDamage = base + random.nextInt(if (special) 9 else 6)
    val fromX = actionUnitX(attacker, if (actionArenaCanvas == null) 1000 else math.max(1, actionArenaCanvas.getWidth)) / (if (actionArenaCanvas == null) 1000.0 else math.max(1, actionArenaCanvas.getWidth).toDouble)
    val toX = actionUnitX(target, if (actionArenaCanvas == null) 1000 else math.max(1, actionArenaCanvas.getWidth)) / (if (actionArenaCanvas == null) 1000.0 else math.max(1, actionArenaCanvas.getWidth).toDouble)
    val hh = if (actionArenaCanvas == null) 500 else math.max(1, actionArenaCanvas.getHeight)
    val fromY = (actionUnitBaseY(attacker, hh) - 80.0) / hh
    val toY = (actionUnitBaseY(target, hh) - 80.0) / hh
    actionArenaProjectiles += ArenaProjectile(0.0, fromX, fromY, toX, toY, attacker.fighter.color.brighter(), target, rawDamage, special)
    if (attacker.side == 0) attacker.assistCooldown = 55
  }

  def tickActionArena(): Unit = {
    if (actionArenaEnded) return
    actionArenaUnits.foreach { u =>
      if (u.attackAnim > 0) u.attackAnim -= 1
      if (u.hitFlash > 0) u.hitFlash -= 1
      if (u.guardTicks > 0) u.guardTicks -= 1
      if (u.cooldown > 0) u.cooldown -= 1
      if (u.energy < 100 && animTick % 7 == 0) u.energy = math.min(100, u.energy + 1)
      if (u.assistCooldown > 0) u.assistCooldown -= 1
      if (u.aiCooldown > 0) u.aiCooldown -= 1
    }

    // Animated projectiles apply damage only when they reach their target.
    val landed = mutable.ArrayBuffer[ArenaProjectile]()
    actionArenaProjectiles.foreach { fx =>
      fx.progress += (if (fx.special) 0.085 else 0.12)
      if (fx.progress >= 1.0) {
        if (!fx.target.defeated) {
          val protection = if (fx.target.guardTicks > 0) 0.48 else math.max(0.55, 1.0 - fx.target.fighter.defense * 0.003)
          val damage = math.max(4, (fx.damage * protection).toInt)
          fx.target.hp = math.max(0, fx.target.hp - damage)
          fx.target.hitFlash = 10
          if (fx.target.hp <= 0) {
            fx.target.defeated = true
            actionArenaMessage = fx.target.fighter.name + " is KO!"
            if (fx.target.side == 0 && fx.target.slot == actionArenaActiveSlot) {
              actionAlive(0).headOption.foreach(u => actionArenaActiveSlot = u.slot)
            }
          } else {
            actionArenaMessage = fx.target.fighter.name + " took " + damage + " damage!"
          }
        }
        landed += fx
      }
    }
    landed.foreach(fx => actionArenaProjectiles -= fx)

    // Enemy robots attack automatically; extra squad heroes provide automatic support.
    actionAlive(1).foreach { enemy =>
      if (enemy.aiCooldown <= 0) {
        val targets = actionAlive(0)
        if (targets.nonEmpty) {
          val active = targets.find(_.slot == actionArenaActiveSlot).getOrElse(targets.minBy(_.hp))
          launchActionArenaAttack(enemy, active, enemy.energy >= 45 && random.nextInt(100) < 23)
          enemy.aiCooldown = 34 + random.nextInt(28)
          if (enemy.energy >= 35 && random.nextInt(100) < 30) enemy.energy -= 35 else enemy.energy = math.min(100, enemy.energy + 8)
        }
      }
    }
    actionAlive(0).filter(_.slot != actionArenaActiveSlot).foreach { ally =>
      if (ally.assistCooldown <= 0) {
        val targets = actionAlive(1)
        if (targets.nonEmpty) {
          val target = targets.minBy(_.hp)
          val special = ally.energy >= 55 && random.nextInt(100) < 20
          launchActionArenaAttack(ally, target, special)
          if (special) ally.energy -= 35 else ally.energy = math.min(100, ally.energy + 7)
          ally.assistCooldown = 44 + random.nextInt(22)
        }
      }
    }

    if (actionAlive(1).isEmpty) finishActionArena(true)
    else if (actionAlive(0).isEmpty) finishActionArena(false)
    refreshActionArenaUI()
  }

  def refreshActionArenaUI(): Unit = {
    if (actionArenaCanvas != null) actionArenaCanvas.repaint()
    if (actionArenaStatus != null) {
      val hero = actionActiveHero
      val summary = if (hero == null) actionArenaMessage else actionArenaMessage + "   |   " + hero.fighter.name + " HP " + hero.hp + "/" + hero.maxHp + "  ENERGY " + hero.energy + "%"
      actionArenaStatus.setText(summary)
    }
    if (actionArenaAttackButton != null) actionArenaAttackButton.setEnabled(!actionArenaEnded && actionActiveHero != null && actionActiveHero.cooldown <= 0)
    if (actionArenaSpecialButton != null) actionArenaSpecialButton.setEnabled(!actionArenaEnded && actionActiveHero != null && actionActiveHero.cooldown <= 0 && actionActiveHero.energy >= 35)
    if (actionArenaGuardButton != null) actionArenaGuardButton.setEnabled(!actionArenaEnded && actionActiveHero != null)
  }

  def finishActionArena(playerWon: Boolean): Unit = {
    if (actionArenaEnded) return
    actionArenaEnded = true
    actionArenaWon = playerWon
    actionArenaMessage = if (playerWon) "VICTORY - ALL ENEMIES KNOCKED OUT!" else "DEFEAT - YOUR SQUAD WAS KNOCKED OUT."
    if (actionArenaTimer != null) actionArenaTimer.stop()
    if (currentAccount != null) {
      currentAccount.battles += 1
      if (playerWon) {
        currentAccount.wins += 1
        actionArenaUnits.filter(_.side == 1).foreach(u => currentAccount.cards += (actionArenaSeries + "|" + u.fighter.name))
      } else currentAccount.losses += 1
      try saveAccount(currentAccount) catch { case _: Exception => () }
    }
    if (actionArenaControls != null) {
      actionArenaControls.removeAll()
      actionArenaControls.add(label(if (playerWon) "VICTORY! CARDS UNLOCKED." else "DEFEAT! TRAIN AND TRY AGAIN.", 15, if (playerWon) new Color(90, 255, 160) else new Color(255, 130, 140)))
      val rematch = button("REMATCH")
      rematch.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = startActionArena(actionArenaMode, actionArenaSeries, actionArenaHeroName) })
      val home = button("BACK TO HOME")
      home.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showHome() })
      actionArenaControls.add(rematch); actionArenaControls.add(home)
      actionArenaControls.revalidate(); actionArenaControls.repaint()
    }
    refreshActionArenaUI()
  }

  // ------------------------------- UNO -------------------------------
  case class UnoCard(colorName: String, value: Int) { def display: String = colorName + " " + value }
  val unoColors = Vector("RED", "BLUE", "GREEN", "YELLOW")
  var unoDeck = mutable.ArrayBuffer[UnoCard]()
  var unoDiscard = mutable.ArrayBuffer[UnoCard]()
  var unoPlayerHand = mutable.ArrayBuffer[UnoCard]()
  var unoRobotHand = mutable.ArrayBuffer[UnoCard]()
  var unoFinished = false
  var unoStatus = ""
  def unoColor(name: String): Color = name match {
    case "RED" => new Color(210, 55, 55); case "BLUE" => new Color(45, 95, 210)
    case "GREEN" => new Color(40, 145, 85); case _ => new Color(235, 205, 55)
  }
  def shuffleUnoDeck(): Unit = {
    val cards = mutable.ArrayBuffer[UnoCard]()
    unoColors.foreach { c => cards += UnoCard(c, 0); for (n <- 1 to 9) { cards += UnoCard(c, n); cards += UnoCard(c, n) } }
    unoDeck.clear(); unoDeck ++= random.shuffle(cards.toVector)
  }
  def startUno(): Unit = {
    closePeer(); isLanBattle = false; unoFinished = false; shuffleUnoDeck(); unoDiscard.clear(); unoPlayerHand.clear(); unoRobotHand.clear()
    for (i <- 0 until 7) { unoPlayerHand += unoDeck.remove(0); unoRobotHand += unoDeck.remove(0) }
    unoDiscard += unoDeck.remove(0); unoStatus = "Match the colour or number."; showUnoGame()
  }
  def unoPlayable(c: UnoCard): Boolean = { val top = unoDiscard.last; c.colorName == top.colorName || c.value == top.value }
  def drawUnoFromDeck(): UnoCard = {
    if (unoDeck.isEmpty && unoDiscard.size > 1) { val top = unoDiscard.last; unoDeck ++= random.shuffle(unoDiscard.dropRight(1).toVector); unoDiscard.clear(); unoDiscard += top }
    if (unoDeck.nonEmpty) unoDeck.remove(0) else UnoCard("RED", 0)
  }
  def showUnoGame(): Unit = {
    val page = new CosmicPanel(new BorderLayout(8, 8)); page.setBorder(new EmptyBorder(12, 15, 12, 15)); page.add(title("BIXUU UNO CARD GAME"), BorderLayout.NORTH)
    val center = plainPanel(new BorderLayout(8, 8)); center.add(label("ROBOT CARDS: " + unoRobotHand.size + "  |  TOP CARD: " + unoDiscard.last.display, 17, gold), BorderLayout.NORTH)
    val handPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 6)); handPanel.setBackground(panelColor)
    for (i <- unoPlayerHand.indices) {
      val c = unoPlayerHand(i); val idx = i; val b = new JButton(c.display); b.setPreferredSize(new Dimension(95, 60)); b.setFont(new Font("Segoe UI", Font.BOLD, 12))
      b.setBackground(unoColor(c.colorName)); b.setForeground(if (c.colorName == "YELLOW") Color.BLACK else Color.WHITE); b.setEnabled(unoPlayable(c))
      b.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = playUnoCard(idx) }); handPanel.add(b)
    }
    val scroll = new JScrollPane(handPanel); scroll.setPreferredSize(new Dimension(780, 170)); center.add(scroll, BorderLayout.CENTER)
    center.add(label(unoStatus, 13, textColor), BorderLayout.SOUTH); page.add(center, BorderLayout.CENTER)
    val footer = new JPanel(new GridLayout(1, 2, 10, 10)); footer.setOpaque(false)
    val draw = button("DRAW CARD"); val back = button("BACK TO HOME"); draw.setEnabled(!unoPlayerHand.exists(unoPlayable))
    draw.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = drawUnoCard() })
    back.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = showHome() })
    footer.add(draw); footer.add(back); page.add(footer, BorderLayout.SOUTH); screen(page, 930, 570)
  }
  def playUnoCard(index: Int): Unit = {
    if (unoFinished || index < 0 || index >= unoPlayerHand.size) return
    val c = unoPlayerHand(index); if (!unoPlayable(c)) return
    unoPlayerHand.remove(index); unoDiscard += c
    if (unoPlayerHand.isEmpty) { finishUno(true); return }
    unoStatus = "You played " + c.display + ". Robot's turn."; robotUnoTurn()
  }
  def drawUnoCard(): Unit = {
    if (unoFinished) return
    if (unoPlayerHand.exists(unoPlayable)) { unoStatus = "You already have a playable card."; showUnoGame() }
    else {
      val c = drawUnoFromDeck(); unoPlayerHand += c
      if (unoPlayable(c)) { unoStatus = "You drew " + c.display + ". You may play it."; showUnoGame() }
      else { unoStatus = "You drew " + c.display + ". Robot's turn."; robotUnoTurn() }
    }
  }
  def robotUnoTurn(): Unit = {
    if (unoFinished) return
    var choices = unoRobotHand.indices.filter(i => unoPlayable(unoRobotHand(i)))
    if (choices.isEmpty) { unoRobotHand += drawUnoFromDeck(); choices = unoRobotHand.indices.filter(i => unoPlayable(unoRobotHand(i))) }
    if (choices.nonEmpty) { val c = unoRobotHand.remove(choices(random.nextInt(choices.size))); unoDiscard += c; unoStatus = "Robot played " + c.display + "."; if (unoRobotHand.isEmpty) { finishUno(false); return } }
    else unoStatus = "Robot cannot play and passes."
    showUnoGame()
  }
  def finishUno(playerWon: Boolean): Unit = {
    if (unoFinished) return; unoFinished = true; currentAccount.battles += 1
    if (playerWon) currentAccount.wins += 1 else currentAccount.losses += 1
    saveAccount(currentAccount); showMessage(if (playerWon) "UNO! YOU WIN!" else "ROBOT WINS THIS UNO GAME!", "UNO RESULT"); showHome()
  }

  def run(): Unit = SwingUtilities.invokeLater(new Runnable { override def run(): Unit = {
    frame = new JFrame("Bixuu Anime Fighting Cards - 3D Edition")
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setMinimumSize(new Dimension(760, 600))
    dataDir.mkdirs()
    val animation = new Timer(33, new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { animTick += 1; if (frame != null) frame.repaint() } })
    animation.start()
    showLogin()
  }})
}

BixuuCardsBattle.run()