Code Sketch


Bixuu ultra 02
By: Mhalsakant School
Category: Programming
object BixuuAllGamesHub {
  import java.awt.{AlphaComposite, BasicStroke, Color, Dimension, Font, GradientPaint, Graphics, Graphics2D, RenderingHints}
  import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, MouseAdapter, MouseEvent}
  import java.awt.geom.RoundRectangle2D
  import javax.swing.{JFrame, JPanel, SwingUtilities, Timer, WindowConstants, JOptionPane}
  import scala.collection.mutable
  import scala.util.Random

  private case class GameInfo(id: String, title: String, category: String, subtitle: String,
                              description: String, accent: Color, emblem: String, controls: String)
  private case class StoryInfo(id: String, title: String, kind: String, subtitle: String,
                               description: String, body: String, accent: Color)
  private case class StoryChapter(part: String, title: String, summary: String)

  private val stories = Vector(
    StoryInfo("harry", "HARRY POTTER: SERIES GUIDE", "FANTASY", "A young wizard discovers a hidden world",
      "A friendly overview of the famous wizarding series.",
      "Harry Potter is a boy who discovers that he belongs to a world of magic. At Hogwarts School of Witchcraft and Wizardry, he learns spells, makes close friends, studies magical subjects and uncovers mysteries about his past. Across the series, Harry, Hermione and Ron face difficult choices and learn that courage, loyalty and kindness matter more than fame or power. This is an original short series overview, not the novels or their chapter text.",
      new Color(174, 142, 255)),
    StoryInfo("arjun", "ARJUN: THE MASTER ARCHER", "EPIC / MYTHOLOGY", "A Mahabharata character story",
      "A retelling of Arjun's lessons in focus, duty and friendship.",
      "Arjun, one of the Pandava brothers in the Mahabharata, becomes known for his discipline and skill with the bow. His teacher Dronacharya recognises his concentration, especially when Arjun focuses on a single target. During the Kurukshetra war, Arjun faces a painful dilemma about fighting people he respects. Krishna guides him through the teachings of the Bhagavad Gita: act with responsibility, seek wisdom and do not let fear alone decide your path. This retelling highlights focus, learning and duty.",
      new Color(255, 196, 104)),
    StoryInfo("monalisa", "MONA LISA: THE MYSTERY OF THE SMILE", "ART & HISTORY", "A painting that travelled through time",
      "Discover Leonardo da Vinci's celebrated portrait.",
      "The Mona Lisa is a portrait painted by Leonardo da Vinci during the Italian Renaissance. Its gentle expression, carefully blended colours and distant landscape have fascinated viewers for centuries. The painting became even more famous after it was stolen from the Louvre in 1911 and later recovered. Today it is displayed at the Louvre Museum in Paris, where visitors study its technique and wonder why the sitter's smile seems to change. This is a short art-history account, not a fictional secret revealed as fact.",
      new Color(127, 220, 177)),
    StoryInfo("titanic", "TITANIC: THE SHIP'S JOURNEY", "TRUE HISTORY", "The liner, its voyage and its legacy",
      "A respectful, non-graphic account of the 1912 disaster.",
      "RMS Titanic was a British passenger liner that began its maiden voyage in April 1912. It travelled from Southampton towards New York, carrying passengers and crew from many backgrounds. On the night of 14 April, the ship struck an iceberg in the North Atlantic and sank in the early hours of 15 April. More than 1,500 people died. The disaster led to major changes in maritime safety, including stronger rules for lifeboats and radio watchkeeping. The people affected deserve to be remembered with respect.",
      new Color(109, 190, 255)),
    StoryInfo("it", "IT: PENNYWISE STORY GUIDE", "MYSTERY / HORROR", "Derry ka rahasya ? A town mystery",
      "PART 1?3 ? 9 short chapters ? Hindi + English mix reader.",
      "In Stephen King's fictional horror story IT, a group of friends in the town of Derry encounter a mysterious shape-shifting presence that often appears as a clown called Pennywise. The friends learn that fear becomes harder to face when they are isolated, but trust and friendship help them stand together. The story moves between their childhood and adult lives as they try to understand the town's repeating mystery. This is a short, non-graphic overview, not the original novel or movie script.",
      new Color(255, 119, 151)),
    StoryInfo("bixuu", "BIXUU: THE THREE WORLDS QUEST", "ORIGINAL ADVENTURE", "An original Bixuu mini-story",
      "A new adventure about games, stories and a helpful robot.",
      "One evening, Bixuu discovers a glowing control panel with three doors: the Game World, the Story Library and the Robot Lab. The first door leads to a spring garden where lost lights need to be collected. The second opens a library whose pages have been scattered by a cosmic wind. The third reveals a friendly robot that can organise clues and help write safe code. Bixuu learns that the worlds work best together: play with imagination, read with curiosity and build with patience. The final key is teamwork.",
      new Color(91, 229, 222)),
    StoryInfo("alice", "ALICE: THE CURIOUS DOOR", "CLASSIC FANTASY", "A strange journey in a wonderland",
      "An original short guide to the public-domain classic.",
      "Alice follows a curious white rabbit and discovers a doorway to a world where ordinary rules behave in surprising ways. She meets unusual characters, solves playful riddles and learns to stay calm when everything around her changes. The adventure is full of wordplay, imagination and questions about growing up. This is a fresh, short retelling of the classic premise, not a copied edition of the book.",
      new Color(151, 194, 255)),
    StoryInfo("sherlock", "SHERLOCK HOLMES: THE LOST CLUE", "DETECTIVE MYSTERY", "A quiet clue solves a puzzle",
      "An original mini-mystery featuring the famous detective.",
      "A visitor arrives at Baker Street with a small puzzle: a valuable notebook has vanished from a locked study. Sherlock Holmes studies the room, listens carefully and notices a thin line of blue dust near the window. Watson first thinks it is nothing, but Holmes explains that the dust came from a newly repaired garden gate. The clue points to a visitor who entered earlier in the day. The notebook is found safely, and Holmes reminds Watson that careful observation is often more useful than a quick guess. This is an original, short Holmes-style mystery summary, not a passage from a Conan Doyle story.",
      new Color(135, 202, 220)),
    StoryInfo("robin", "ROBIN HOOD: THE GOLDEN ARROW", "FOLK ADVENTURE", "A contest in Sherwood Forest",
      "A fresh retelling of the well-known folk legend.",
      "In Sherwood Forest, Robin Hood hears that a village is struggling under an unfair tax. Rather than start a fight, he enters the sheriff's archery contest in disguise. His steady aim wins the golden arrow, and he uses the prize ceremony to reveal the villagers' true story. The people are heard at last, and the sheriff must answer for his unfair choices. Robin's companions celebrate not just his skill, but his decision to stand beside people who need help. This is a new, gentle retelling of a traditional legend.",
      new Color(113, 210, 153)),
    StoryInfo("panchatantra", "PANCHATANTRA: THE CLEVER RABBIT", "INDIAN FABLE", "Wisdom can be stronger than size",
      "A short retelling of a traditional animal tale.",
      "A proud lion frightens the animals of a forest. A small rabbit volunteers to speak with him and arrives late, explaining that another lion claimed to be the real king. Curious and angry, the lion follows the rabbit to a deep well. Seeing his own reflection, he mistakes it for a rival and learns that pride can make a person foolish. The forest becomes peaceful again. The lesson is that calm thinking and courage can solve problems that strength alone cannot.",
      new Color(255, 190, 105)),
    StoryInfo("tenali", "TENALI RAMAN: THE WISE ANSWER", "INDIAN FOLKTALE", "A clever answer at the royal court",
      "A light-hearted story about wit and fairness.",
      "At the king's court, two neighbours argue over a fruit tree that grows between their homes. Each claims every fruit belongs to them. Tenali Raman listens, then asks them to water the tree together for one week and collect only the fruit that naturally falls on each side. The neighbours realise how much effort a shared tree needs and agree to care for it together. The king praises Tenali for finding a fair solution instead of choosing a winner. This is an original retelling inspired by Tenali Raman folklore.",
      new Color(245, 177, 102)),
    StoryInfo("birbal", "AKBAR & BIRBAL: THE SIMPLE QUESTION", "INDIAN FOLKTALE", "A puzzle solved with patience",
      "A short court tale about clear thinking.",
      "Emperor Akbar asks his court how to make a line shorter without touching it. Many ministers offer complicated answers. Birbal simply draws a much longer line beside it. The first line now looks shorter, though it has not changed at all. Akbar smiles at the clever demonstration. The small puzzle reminds the court that a new point of view can change how a problem appears. This is a newly written version of a traditional Akbar-Birbal-style tale.",
      new Color(236, 197, 111)),
    StoryInfo("savitri", "SAVITRI: COURAGE AND WISDOM", "INDIAN EPIC", "A story of devotion and determination",
      "A respectful retelling from the Mahabharata tradition.",
      "Savitri chooses Satyavan as her life partner, knowing that a difficult fate has been foretold. When the day of danger arrives, she meets it with courage and follows the path of wisdom rather than giving in to despair. Through thoughtful words and steadfast resolve, she wins a chance for Satyavan to return. The tale is remembered for Savitri's intelligence, loyalty and strength of character. This short retelling focuses on the traditional story's values.",
      new Color(255, 163, 130)),
    StoryInfo("lionmouse", "THE LION AND THE MOUSE", "AESOP FABLE", "A small friend can make a big difference",
      "A classic fable retold in simple language.",
      "A lion allows a tiny mouse to go free. Later, the lion becomes caught in a hunter's net. Hearing the lion's call, the mouse rushes over and carefully gnaws through the ropes. The lion is freed and understands that kindness is never wasted. The smallest creature can offer help that a mighty one cannot provide. The fable teaches gratitude, friendship and respect for every living being.",
      new Color(255, 198, 117)),
    StoryInfo("hare", "THE TORTOISE AND THE HARE", "AESOP FABLE", "Steady effort reaches the finish",
      "A well-known race with a timeless lesson.",
      "A speedy hare laughs at a tortoise for moving slowly and challenges it to a race. Confident that victory is certain, the hare stops to rest. The tortoise keeps moving at a steady pace and reaches the finish first. The hare learns that talent is not enough without attention and effort. The story celebrates patience, consistency and the value of completing what you begin.",
      new Color(145, 215, 157)),
    StoryInfo("cinderella", "CINDERELLA: A KIND HEART", "FAIRY TALE", "A new beginning after a royal celebration",
      "A gentle retelling of a traditional fairy tale.",
      "Cinderella is treated unfairly by her household, yet she continues to act with kindness. With magical help, she attends a royal celebration and meets a young prince. When the evening ends, she leaves behind a glass slipper. The slipper later helps the royal household find her. Cinderella's life changes, but the most important part of her character remains her hopeful and generous spirit. This is an original, simplified retelling of a traditional public-domain fairy tale.",
      new Color(177, 159, 255)),
    StoryInfo("starquest", "BIXUU AND THE LOST STAR", "ORIGINAL SPACE ADVENTURE", "A glowing star needs a guide",
      "An original story about courage and helping others.",
      "During a quiet night, Bixuu notices a small star resting on the hill beside the town. It has lost its way home. Bixuu builds a simple signal lamp with the help of a friendly robot and climbs to the old observatory. Together they send a pattern of light into the sky. A trail of stars answers, showing the little visitor the way back to its constellation. Bixuu learns that even a small act of help can brighten a very large universe.",
      new Color(120, 173, 255)),
    StoryInfo("robotlibrary", "RIO THE ROBOT AND THE LOST LIBRARY", "ORIGINAL ROBOT STORY", "A library of floating pages",
      "An original mystery about reading and teamwork.",
      "Rio is a small library robot whose shelves are filled with maps, poems and stories. One morning, a warm wind sends pages spinning through the city. Rio cannot collect them all alone, so Bixuu asks neighbours to help. A baker finds a map, a gardener finds a poem and a child finds the final page beneath a bench. When the pages are returned, Rio creates a new shelf called The Stories We Share. The library becomes a place where everyone can add a tale.",
      new Color(102, 224, 211)),
    StoryInfo("clockwork", "BIXUU: THE CLOCKWORK CITY", "ORIGINAL ADVENTURE", "A city whose clocks have stopped",
      "An original puzzle adventure in a mechanical city.",
      "Bixuu reaches a city where every clock has stopped at the same minute. A tiny mechanic explains that the central tower has lost its rhythm key. Bixuu explores the quiet streets, solves three simple gear puzzles and finds the key beneath the old bridge. Instead of keeping the discovery secret, Bixuu invites the townspeople to restart the tower together. When the bells ring again, the city celebrates a lesson in cooperation: every small gear matters.",
      new Color(255, 174, 111)),
    StoryInfo("oceans", "THE BLUE OCEAN PROMISE", "ORIGINAL NATURE STORY", "A beach clean-up becomes an adventure",
      "An original story about caring for nature.",
      "Bixuu and two friends visit a quiet beach and find that plastic waste has covered part of the sand. They organise a safe clean-up with adults, sort the rubbish and place a sign asking visitors to protect the shore. A local scientist shows them tiny sea creatures living in the rock pools. The friends decide to return every month and invite more people to help. Their promise is simple: enjoy nature, learn about it and leave it cleaner than they found it.",
      new Color(94, 192, 234)),
    StoryInfo("mountain", "THE MOUNTAIN OF SEVEN LIGHTS", "ORIGINAL FANTASY", "Seven clues lead to a hidden garden",
      "An original adventure with riddles and teamwork.",
      "A map leads Bixuu and a group of friends to a mountain said to hold seven coloured lights. Each light is protected by a riddle about patience, honesty, sharing, courage, curiosity, kindness and teamwork. No single friend can solve every clue, but together they find all seven. At the summit they discover a garden that shines at sunset. The treasure is not gold; it is a place that grows brighter whenever visitors help one another.",
      new Color(194, 148, 255))
  )

  // Original, short, non-graphic companion summaries. These are NOT chapters from
  // Stephen King's copyrighted novel or any film script.
  private val pennywiseChapters = Vector(
    StoryChapter("PART 1 ? DERRY KA RAAZ / THE TOWN MYSTERY", "CHAPTER 1 ? DERRY: EK AJEEB TOWN",
      "Derry ek fictional town hai jahan log kabhi-kabhi ajeeb incidents notice karte hain. Is guide mein hum town ke clues ko calmly explore karenge. Yeh original summary hai, novel ka copied text nahi."),
    StoryChapter("PART 1 ? DERRY KA RAAZ / THE TOWN MYSTERY", "CHAPTER 2 ? PENNYWISE KA MYSTERY",
      "Pennywise ek mysterious, shape-shifting fictional character hai. Uska clown roop story ka ek symbol hai; is chapter mein hum character ke mystery-role ko samajhte hain, bina scary ya graphic details ke."),
    StoryChapter("PART 1 ? DERRY KA RAAZ / THE TOWN MYSTERY", "CHAPTER 3 ? FRIENDS KI TEAM",
      "Kuch young friends apne town ke strange clues par baat karte hain. Teamwork se unhe samajh aata hai ki problems ko akela face karne ke bajay trusted friends ke saath discuss karna better hota hai."),
    StoryChapter("PART 2 ? CLUES AUR YAADEN / CLUES & MEMORIES", "CHAPTER 4 ? PURANE CLUES",
      "Friends alag-alag memories aur observations ko jodte hain. Har clue unhe Derry ke mystery ke baare mein ek naya question deta hai. Kahani ka focus curiosity, observation aur careful thinking par hai."),
    StoryChapter("PART 2 ? CLUES AUR YAADEN / CLUES & MEMORIES", "CHAPTER 5 ? DAR KO SAMAJHNA",
      "Team realise karti hai ki fear ko samajhna aur us par baat karna important hai. Woh ek-doosre ki baat sunte hain aur plan banate hain. Yeh chapter courage aur emotional support ka message deta hai."),
    StoryChapter("PART 2 ? CLUES AUR YAADEN / CLUES & MEMORIES", "CHAPTER 6 ? DERRY SE DOOR",
      "Time ke saath friends apni-apni lives mein aage badhte hain. Derry ki memories alag ho sakti hain, lekin friendship aur shared experiences unke liye meaningful rehte hain."),
    StoryChapter("PART 3 ? HIMMAT AUR DOSTI / COURAGE & FRIENDSHIP", "CHAPTER 7 ? WAPAS MILNA",
      "Kai saalon baad purane friends phir connect karte hain. Woh calmly apni memories share karte hain aur decide karte hain ki mystery ko samajhne ke liye ek-doosre ka saath denge."),
    StoryChapter("PART 3 ? HIMMAT AUR DOSTI / COURAGE & FRIENDSHIP", "CHAPTER 8 ? FINAL MYSTERY KE CLUES",
      "Friends apne collected clues ko compare karte hain. Unhe pata chalta hai ki courage ka matlab careless hona nahi; safe choices, clear thinking aur teamwork bhi courage ka part hain."),
    StoryChapter("PART 3 ? HIMMAT AUR DOSTI / COURAGE & FRIENDSHIP", "CHAPTER 9 ? DOSTI KI JEET",
      "Guide ka conclusion friendship, trust aur hope par focus karta hai. Derry ki mystery ek reminder ban jaati hai ki difficult feelings ko trusted people ke saath share karna aur help lena strength hai."),
  )

  private val games = Vector(
    GameInfo("spring", "SANCTUS: ETERNAL BLOOM", "ADVENTURE", "BIXUU SPRING SEASON",
      "Explore glowing gardens, collect crystals, discover portals and play with friends on LAN.",
      new Color(90, 238, 190), "BLOOM", "WASD / arrows ? E interact ? Space dash"),
    GameInfo("sanctus3d", "SANCTUS FOURTH EVENT", "ADVENTURE", "3D-STYLE REALM QUEST",
      "Enter the Sanctus realms, unlock powers, gather gems and progress through the event.",
      new Color(112, 190, 255), "REALM", "WASD / arrows ? F fight ? E collect"),
    GameInfo("sanctusmp", "SANCTUS COMBAT ARENA", "ACTION", "MULTIPLAYER EDITION",
      "A separate combat-event build with realm battles, powers and local-network play.",
      new Color(255, 132, 174), "ARENA", "WASD / arrows ? F fight ? R power"),
    GameInfo("cyber", "CYBER BLOOM: GLITCH GARDEN", "ADVENTURE", "SPRING SEASON MINI-GAME",
      "Restore a digital garden, collect seeds and bloom flowers in a neon world.",
      new Color(184, 123, 255), "CYBER", "Arrow keys / WASD ? click to start"),
    GameInfo("diwali", "DIWALI TRAIN ADVENTURE", "ADVENTURE", "BIXUU FESTIVAL JOURNEY",
      "Travel by train, complete family missions, collect items and celebrate Diwali.",
      new Color(255, 190, 86), "DIWALI", "WASD / arrows ? E / Enter interact"),
    GameInfo("cards", "ANIME FIGHTING CARDS", "FIGHTING", "BIXUU 3D CARD BATTLE",
      "Build your card battle, challenge the robot and explore the fighting-card modes.",
      new Color(255, 113, 126), "DUEL", "Mouse controls ? follow on-screen prompts")
  )

  private val favorites = mutable.Set[String]()
  private var currentPage = "HOME"
  private var selectedCategory = "ALL"
  private var searchText = ""
  private var searchFocused = false
  private var frame: JFrame = _
  private var panel: HubPanel = _

  def main(args: Array[String]): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = createWindow()
    })
  }

  private def createWindow(): Unit = {
    frame = new JFrame("BIXUU'S ALL GAMES | ULTRA GAME HUB")
    panel = new HubPanel
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
    frame.setContentPane(panel)
    frame.setMinimumSize(new Dimension(1040, 740))
    frame.setSize(1380, 900)
    frame.setLocationRelativeTo(null)
    frame.setVisible(true)
    panel.requestFocusInWindow()
    val timer = new Timer(16, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        panel.advanceAnimation()
        panel.repaint()
      }
    })
    timer.start()
  }

  private def launchGame(id: String): Unit = {
    try {
      id match {
        case "spring"   => BixuuSpringSeasonMultiplayer.main(new Array[String](0))
        case "sanctus3d" => SanctusFourthEvent.main(new Array[String](0))
        case "sanctusmp" => SanctusFourthEventCombat.main(new Array[String](0))
        case "cyber"    => SpringSeason_CyberBloomGlitchInTheGarden.main(new Array[String](0))
        case "diwali"   => BixuuDiwaliGame.main(new Array[String](0))
        case "cards"    => BixuuCardsBattle.run()
        case _ => ()
      }
    } catch {
      case ex: Throwable =>
        JOptionPane.showMessageDialog(frame,
          "This game could not be opened.\n" + Option(ex.getMessage).getOrElse(ex.getClass.getSimpleName),
          "BIXUU GAME HUB", JOptionPane.ERROR_MESSAGE)
    }
  }

  private class HubPanel extends JPanel {
    private case class Particle(var x: Double, var y: Double, var speed: Double, var radius: Double, var alpha: Float)
    private val rng = new Random(20261031L)
    private val particles = Array.fill(95)(Particle(rng.nextDouble() * 1500.0, rng.nextDouble() * 1000.0,
      0.25 + rng.nextDouble() * 0.9, 1.0 + rng.nextDouble() * 2.8, (0.12 + rng.nextDouble() * 0.42).toFloat))
    private var tick = 0
    private var mouseX = -1
    private var mouseY = -1
    private var hoverCard = -1
    private var hoverPlay = -1
    private var hoverStar = -1
    private var selectedCard = -1
    private var searchBounds = new java.awt.Rectangle(0, 0, 0, 0)
    private var heroPlayBounds = new java.awt.Rectangle(0, 0, 0, 0)
    private var navBounds = Array.empty[java.awt.Rectangle]
    private var categoryBounds = Array.empty[java.awt.Rectangle]
    private var cardBounds = Array.empty[java.awt.Rectangle]
    private var playBounds = Array.empty[java.awt.Rectangle]
    private var starBounds = Array.empty[java.awt.Rectangle]
    private var storyBounds = Array.empty[java.awt.Rectangle]
    private var storyPrevBounds = new java.awt.Rectangle(0, 0, 0, 0)
    private var storyNextBounds = new java.awt.Rectangle(0, 0, 0, 0)
    private var storyPage = 0
    private val storyPageSize = 6
    private var robotBounds = Array.empty[java.awt.Rectangle]

    setFocusable(true)
    setBackground(new Color(7, 10, 25))
    addMouseMotionListener(new MouseAdapter {
      override def mouseMoved(e: MouseEvent): Unit = {
        mouseX = e.getX; mouseY = e.getY
        calculateHover(); repaint()
      }
      override def mouseDragged(e: MouseEvent): Unit = mouseMoved(e)
    })
    addMouseListener(new MouseAdapter {
      override def mouseExited(e: MouseEvent): Unit = { mouseX = -1; mouseY = -1; calculateHover(); repaint() }
      override def mouseClicked(e: MouseEvent): Unit = handleClick(e.getX, e.getY)
    })
    addKeyListener(new KeyAdapter {
      override def keyPressed(e: KeyEvent): Unit = {
        if (e.getKeyCode == KeyEvent.VK_ESCAPE) {
          if (searchFocused) { searchFocused = false; searchText = "" }
          else { currentPage = "HOME"; selectedCategory = "ALL" }
        } else if (e.getKeyCode == KeyEvent.VK_BACK_SPACE && searchFocused) {
          if (searchText.nonEmpty) searchText = searchText.dropRight(1)
        } else if (e.getKeyCode >= KeyEvent.VK_1 && e.getKeyCode <= KeyEvent.VK_6 && !searchFocused) {
          val idx = e.getKeyCode - KeyEvent.VK_1
          if (idx >= 0 && idx < games.length) launchGame(games(idx).id)
        } else if (e.getKeyCode == KeyEvent.VK_F && hoverCard >= 0 && hoverCard < visibleGames.length) {
          toggleFavorite(visibleGames(hoverCard).id)
        } else if (e.getKeyCode == KeyEvent.VK_ENTER && searchFocused) {
          searchFocused = false
        }
        calculateHover(); repaint()
      }
      override def keyTyped(e: KeyEvent): Unit = {
        if (searchFocused && !Character.isISOControl(e.getKeyChar) && searchText.length < 28) {
          searchText += e.getKeyChar
          calculateHover(); repaint()
        }
      }
    })

    def advanceAnimation(): Unit = {
      tick += 1
      val w = math.max(1, getWidth).toDouble
      val h = math.max(1, getHeight).toDouble
      particles.foreach { p =>
        p.y -= p.speed
        p.x += math.sin((tick * 0.012) + p.y * 0.01) * 0.22
        if (p.y < -8) { p.y = h + 8; p.x = rng.nextDouble() * w }
        if (p.x < 0) p.x = w
        if (p.x > w) p.x = 0
      }
    }

    private def visibleGames: Vector[GameInfo] = {
      var result = if (currentPage == "FAVOURITES") games.filter(g => favorites.contains(g.id)) else games
      if (selectedCategory != "ALL") result = result.filter(_.category == selectedCategory)
      val q = searchText.trim.toLowerCase
      if (q.nonEmpty) result = result.filter(g => (g.title + " " + g.subtitle + " " + g.category + " " + g.description).toLowerCase.contains(q))
      result
    }

    private def visibleStories: Vector[StoryInfo] = {
      val start = storyPage * storyPageSize
      stories.slice(start, start + storyPageSize)
    }

    private def toggleFavorite(id: String): Unit = {
      if (favorites.contains(id)) favorites -= id else favorites += id
    }

    private def rounded(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, r: Int, c: Color): Unit = {
      g.setColor(c); g.fillRoundRect(x, y, w, h, r, r)
    }

    private def drawCentered(g: Graphics2D, value: String, x: Int, y: Int, w: Int): Unit = {
      val fm = g.getFontMetrics; g.drawString(value, x + (w - fm.stringWidth(value)) / 2, y)
    }

    private def gradient(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, a: Color, b: Color, radius: Int): Unit = {
      g.setPaint(new GradientPaint(x.toFloat, y.toFloat, a, (x + w).toFloat, (y + h).toFloat, b))
      g.fillRoundRect(x, y, w, h, radius, radius)
    }

    override def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val g = graphics.create().asInstanceOf[Graphics2D]
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
      g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
      val w = getWidth; val h = getHeight
      g.setPaint(new GradientPaint(0, 0, new Color(8, 12, 31), w, h, new Color(17, 12, 43)))
      g.fillRect(0, 0, w, h)
      drawAmbient(g, w, h)
      drawSidebar(g, h)
      drawHeader(g, w)
      if (currentPage == "STORIES") {
        drawStoryPage(g, w, h)
      } else if (currentPage == "ROBOT") {
        drawRobotPage(g, w, h)
      } else {
        drawHero(g, w)
        drawCategories(g, w)
        drawGameCards(g, w, h)
      }
      drawFooter(g, w, h)
      g.dispose()
    }

    private def drawAmbient(g: Graphics2D, w: Int, h: Int): Unit = {
      val glow = new java.awt.RadialGradientPaint(220, 160, 300,
        Array(0.0f, 1.0f), Array(new Color(62, 47, 160, 40), new Color(62, 47, 160, 0)))
      g.setPaint(glow); g.fillRect(0, 0, w, h)
      val glow2 = new java.awt.RadialGradientPaint(math.max(250, w - 80), h - 80, 280,
        Array(0.0f, 1.0f), Array(new Color(0, 185, 201, 25), new Color(0, 185, 201, 0)))
      g.setPaint(glow2); g.fillRect(0, 0, w, h)
      particles.foreach { p =>
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, p.alpha))
        g.setColor(new Color(118, 205, 255))
        g.fillOval(p.x.toInt, p.y.toInt, p.radius.toInt + 1, p.radius.toInt + 1)
      }
      g.setComposite(AlphaComposite.SrcOver)
    }

    private def drawSidebar(g: Graphics2D, h: Int): Unit = {
      g.setColor(new Color(8, 13, 32, 235)); g.fillRect(0, 0, 218, h)
      g.setColor(new Color(102, 128, 190, 55)); g.drawLine(217, 0, 217, h)
      g.setFont(new Font("SansSerif", Font.BOLD, 27)); g.setColor(new Color(246, 248, 255)); g.drawString("BIXUU", 28, 54)
      g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(new Color(97, 224, 245)); g.drawString("GAMES ? STORIES ? ROBOT", 30, 74)
      g.setColor(new Color(80, 104, 157, 65)); g.drawLine(24, 100, 192, 100)
      val items = Vector(("HOME", "01"), ("ALL GAMES", "06"), ("FAVOURITES", "?"),
        ("STORY READING", "05"), ("ROBOT / CODE", "AI"))
      navBounds = new Array[java.awt.Rectangle](items.length)
      var i = 0
      items.foreach { item =>
        val y = 124 + i * 56
        val rect = new java.awt.Rectangle(18, y, 182, 46); navBounds(i) = rect
        val active = (i == 0 && currentPage == "HOME") || (i == 1 && currentPage == "ALL GAMES") ||
          (i == 2 && currentPage == "FAVOURITES") || (i == 3 && currentPage == "STORIES") ||
          (i == 4 && currentPage == "ROBOT")
        if (active || rect.contains(mouseX, mouseY)) {
          gradient(g, rect.x, rect.y, rect.width, rect.height,
            new Color(62, 72, 160, 175), new Color(19, 145, 171, 110), 14)
          g.setColor(new Color(110, 225, 255, 100)); g.drawRoundRect(rect.x, rect.y, rect.width, rect.height, 14, 14)
        }
        g.setFont(new Font("SansSerif", Font.BOLD, 12)); g.setColor(if (active) new Color(255,255,255) else new Color(172,185,216))
        g.drawString(item._1, 48, y + 29)
        g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(new Color(122, 145, 195)); g.drawString(item._2, 166, y + 28)
        i += 1
      }
      val bottom = h - 110
      rounded(g, 18, bottom, 182, 82, 16, new Color(20, 31, 64, 205))
      g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(new Color(112, 234, 222)); g.drawString("YOUR COLLECTION", 32, bottom + 25)
      g.setFont(new Font("SansSerif", Font.BOLD, 23)); g.setColor(Color.WHITE); g.drawString("" + games.length, 32, bottom + 55)
      g.setFont(new Font("SansSerif", Font.PLAIN, 10)); g.setColor(new Color(160, 177, 211)); g.drawString("GAMES  ?  STORIES  ?  ROBOT", 68, bottom + 53)
    }

    private def drawHeader(g: Graphics2D, w: Int): Unit = {
      val heading = if (currentPage == "FAVOURITES") "YOUR FAVOURITES"
        else if (currentPage == "HOME") "WELCOME TO THE UNIVERSE"
        else if (currentPage == "STORIES") "BIXUU STORY READING"
        else if (currentPage == "ROBOT") "BIXUU ROBOT & CODE LAB"
        else "ALL GAMES"
      g.setFont(new Font("SansSerif", Font.BOLD, 23)); g.setColor(new Color(248, 250, 255)); g.drawString(heading, 252, 47)
      val sub = if (currentPage == "STORIES") "Choose a story, open its reader and explore." 
        else if (currentPage == "ROBOT") "Ask the helper, browse game modules or build a mission idea."
        else "Choose a world. Press play. Make it yours."
      g.setFont(new Font("SansSerif", Font.PLAIN, 11)); g.setColor(new Color(151, 169, 205)); g.drawString(sub, 253, 69)
      if (currentPage == "STORIES" || currentPage == "ROBOT") {
        searchBounds = new java.awt.Rectangle(0, 0, 0, 0)
      } else {
        val sx = w - 350; val sy = 26
        searchBounds = new java.awt.Rectangle(sx, sy, 318, 46)
        rounded(g, sx, sy, 318, 46, 14, if (searchFocused) new Color(30, 47, 90) else new Color(20, 29, 59))
        g.setColor(if (searchFocused) new Color(106, 226, 255) else new Color(65, 85, 139)); g.drawRoundRect(sx, sy, 318, 46, 14, 14)
        g.setFont(new Font("SansSerif", Font.PLAIN, 17)); g.setColor(new Color(131, 159, 200)); g.drawString("?", sx + 15, sy + 29)
        g.setFont(new Font("SansSerif", Font.PLAIN, 12)); g.setColor(if (searchText.nonEmpty) Color.WHITE else new Color(139, 157, 190))
        val query = if (searchText.nonEmpty) searchText else "Search games..."
        g.drawString(query + (if (searchFocused && (tick / 25) % 2 == 0) "|" else ""), sx + 45, sy + 28)
      }
    }

    private def drawHero(g: Graphics2D, w: Int): Unit = {
      val x = 250; val y = 102; val bw = w - 286; val bh = 155
      gradient(g, x, y, bw, bh, new Color(36, 34, 106), new Color(9, 93, 117), 24)
      g.setColor(new Color(141, 195, 255, 95)); g.setStroke(new BasicStroke(1.2f)); g.drawRoundRect(x, y, bw, bh, 24, 24)
      g.setColor(new Color(255,255,255,12)); g.fillOval(x + bw - 240, y - 60, 280, 280)
      g.setColor(new Color(79, 231, 236, 28)); g.fillOval(x + bw - 175, y + 10, 155, 155)
      g.setColor(new Color(142, 132, 255, 130)); g.setStroke(new BasicStroke(2.2f)); g.drawOval(x + bw - 205, y - 24, 220, 205)
      g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(new Color(129, 246, 229)); g.drawString("THE COMPLETE BIXUU UNIVERSE", x + 28, y + 30)
      g.setFont(new Font("SansSerif", Font.BOLD, 30)); g.setColor(Color.WHITE); g.drawString("BIXUU UNIVERSE", x + 28, y + 70)
      g.setFont(new Font("SansSerif", Font.PLAIN, 12)); g.setColor(new Color(216, 228, 255)); g.drawString("Games, stories and robot tools ? all in one place.", x + 30, y + 96)
      heroPlayBounds = new java.awt.Rectangle(x + 28, y + 112, 156, 32)
      gradient(g, heroPlayBounds.x, heroPlayBounds.y, heroPlayBounds.width, heroPlayBounds.height,
        new Color(88, 235, 210), new Color(77, 160, 255), 12)
      g.setFont(new Font("SansSerif", Font.BOLD, 11)); g.setColor(new Color(7, 21, 45)); drawCentered(g, "PLAY FEATURED  ?", heroPlayBounds.x, heroPlayBounds.y + 21, heroPlayBounds.width)
      g.setFont(new Font("SansSerif", Font.BOLD, 13)); g.setColor(new Color(232, 244, 255)); g.drawString("BIXUU", x + bw - 151, y + 85)
      g.setFont(new Font("SansSerif", Font.PLAIN, 9)); g.setColor(new Color(183, 218, 245)); g.drawString("GAMES ? STORIES ? AI", x + bw - 151, y + 103)
    }

    private def drawCategories(g: Graphics2D, w: Int): Unit = {
      g.setFont(new Font("SansSerif", Font.BOLD, 13)); g.setColor(new Color(235, 241, 255)); g.drawString("EXPLORE CATEGORIES", 252, 293)
      val cats = Vector("ALL", "ADVENTURE", "ACTION", "FIGHTING", "FESTIVAL")
      categoryBounds = new Array[java.awt.Rectangle](cats.length)
      var x = 252
      var i = 0
      cats.foreach { c =>
        val cw = if (c == "ADVENTURE") 105 else if (c == "FIGHTING") 91 else if (c == "FESTIVAL") 82 else 75
        val rect = new java.awt.Rectangle(x, 307, cw, 30); categoryBounds(i) = rect
        val active = selectedCategory == c
        rounded(g, rect.x, rect.y, rect.width, rect.height, 13,
          if (active) new Color(74, 114, 197, 200) else new Color(30, 39, 72, 220))
        if (active) { g.setColor(new Color(116, 224, 255)); g.drawRoundRect(rect.x, rect.y, rect.width, rect.height, 13, 13) }
        g.setFont(new Font("SansSerif", Font.BOLD, 9)); g.setColor(if (active) Color.WHITE else new Color(160, 175, 211)); drawCentered(g, c, rect.x, rect.y + 20, rect.width)
        x += cw + 9; i += 1
      }
      g.setFont(new Font("SansSerif", Font.PLAIN, 10)); g.setColor(new Color(126, 149, 190))
      g.drawString(visibleGames.length + " GAMES AVAILABLE", w - 190, 327)
    }

    private def drawGameCards(g: Graphics2D, w: Int, h: Int): Unit = {
      val list = visibleGames
      val left = 250; val gap = 15; val total = w - 286
      val cw = math.max(210, (total - gap * 2) / 3); val ch = 166; val top = 352; val rowGap = 15
      cardBounds = new Array[java.awt.Rectangle](list.length)
      playBounds = new Array[java.awt.Rectangle](list.length)
      starBounds = new Array[java.awt.Rectangle](list.length)
      if (list.isEmpty) {
        g.setFont(new Font("SansSerif", Font.BOLD, 18)); g.setColor(new Color(210, 222, 248));
        g.drawString("No games found", left + 30, top + 55)
        g.setFont(new Font("SansSerif", Font.PLAIN, 12)); g.setColor(new Color(140, 158, 194));
        g.drawString("Try another search or select a different category.", left + 30, top + 82)
      }
      list.zipWithIndex.foreach { pair =>
        val game = pair._1; val i = pair._2
        val col = i % 3; val row = i / 3
        val x = left + col * (cw + gap); val y = top + row * (ch + rowGap)
        val rect = new java.awt.Rectangle(x, y, cw, ch); cardBounds(i) = rect
        val hovering = i == hoverCard
        rounded(g, x + 2, y + 5, cw, ch, 18, new Color(0, 0, 0, 80))
        gradient(g, x, y, cw, ch,
          if (hovering) new Color(38, 52, 94) else new Color(21, 29, 58),
          new Color(12, 20, 43), 18)
        g.setColor(new Color(game.accent.getRed, game.accent.getGreen, game.accent.getBlue, if (hovering) 180 else 90))
        g.setStroke(new BasicStroke(if (hovering) 1.8f else 1.0f)); g.drawRoundRect(x, y, cw, ch, 18, 18)
        g.setColor(game.accent); g.fillRoundRect(x + 13, y + 13, 4, ch - 26, 5, 5)
        g.setColor(new Color(game.accent.getRed, game.accent.getGreen, game.accent.getBlue, 28))
        g.fillOval(x + cw - 82, y + 16, 68, 68)
        g.setFont(new Font("SansSerif", Font.BOLD, 9)); g.setColor(game.accent); g.drawString(game.emblem, x + cw - 73, y + 55)
        val star = new java.awt.Rectangle(x + cw - 35, y + 12, 23, 23); starBounds(i) = star
        g.setFont(new Font("SansSerif", Font.BOLD, 18)); g.setColor(if (favorites.contains(game.id)) new Color(255, 218, 110) else new Color(108, 124, 164))
        g.drawString(if (favorites.contains(game.id)) "?" else "?", star.x + 2, star.y + 18)
        g.setFont(new Font("SansSerif", Font.BOLD, 8)); g.setColor(new Color(156, 176, 211)); g.drawString(game.category, x + 27, y + 31)
        g.setFont(new Font("SansSerif", Font.BOLD, 12)); g.setColor(Color.WHITE)
        val title = if (game.title.length > 25) game.title.substring(0, 24) + "?" else game.title
        g.drawString(title, x + 27, y + 60)
        g.setFont(new Font("SansSerif", Font.BOLD, 8)); g.setColor(game.accent); g.drawString(game.subtitle, x + 27, y + 77)
        g.setFont(new Font("SansSerif", Font.PLAIN, 9)); g.setColor(new Color(170, 185, 216))
        val shortDesc = if (game.description.length > 75) game.description.substring(0, 72) + "..." else game.description
        g.drawString(shortDesc, x + 27, y + 98)
        val btn = new java.awt.Rectangle(x + 27, y + 119, 105, 31); playBounds(i) = btn
        gradient(g, btn.x, btn.y, btn.width, btn.height,
          if (i == hoverPlay) new Color(game.accent.getRed, game.accent.getGreen, game.accent.getBlue) else new Color(game.accent.getRed, game.accent.getGreen, game.accent.getBlue, 205),
          new Color(81, 128, 206), 10)
        g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(new Color(7, 17, 39)); drawCentered(g, "PLAY  ?", btn.x, btn.y + 20, btn.width)
        g.setFont(new Font("SansSerif", Font.PLAIN, 8)); g.setColor(new Color(134, 154, 193)); g.drawString("BIXUU ORIGINAL", x + cw - 100, y + 139)
      }
    }

    private def storyHindiLine(id: String): String = id match {
      case "harry" => "Jadoo ki duniya ? Magic world"
      case "arjun" => "Dhanush, focus aur dharma ? Focus & duty"
      case "monalisa" => "Muskurahat ka rahasya ? The famous smile"
      case "titanic" => "Safar aur itihaas ? Voyage & history"
      case "it" => "Derry ka raaz ? Part-wise chapter guide"
      case "bixuu" => "Teen duniyaon ka adventure ? Three worlds"
      case "alice" => "Ajab duniya ka safar ? Wonderland journey"
      case "sherlock" => "Khoi hui clue ? The missing clue"
      case "robin" => "Sunehra teer ? The golden arrow"
      case "panchatantra" => "Chhote khargosh ki buddhi ? Clever rabbit"
      case "tenali" => "Tenali ka smart jawab ? A wise answer"
      case "birbal" => "Akbar-Birbal ka puzzle ? A simple question"
      case "savitri" => "Himmat aur samajhdari ? Courage & wisdom"
      case "lionmouse" => "Sher aur chuha ? Lion & mouse"
      case "hare" => "Kachhua aur khargosh ? Tortoise & hare"
      case "cinderella" => "Dayalu dil ? A kind heart"
      case "starquest" => "Khoya hua tara ? The lost star"
      case "robotlibrary" => "Robot ki library ? The lost library"
      case "clockwork" => "Ghadiyon ka shehar ? Clockwork city"
      case "oceans" => "Neela samundar ? Blue ocean promise"
      case "mountain" => "Saat roshniyon ka pahad ? Seven lights"
      case _ => "Hindi + English story"
    }

    private def drawStoryPage(g: Graphics2D, w: Int, h: Int): Unit = {
      val x = 250; val top = 105; val bw = w - 286
      gradient(g, x, top, bw, 116, new Color(51, 30, 96), new Color(9, 91, 112), 22)
      g.setColor(new Color(150, 213, 255, 85)); g.drawRoundRect(x, top, bw, 116, 22, 22)
      g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(new Color(124, 245, 223))
      g.drawString("READ ? DISCOVER ? IMAGINE", x + 25, top + 27)
      g.setFont(new Font("SansSerif", Font.BOLD, 24)); g.setColor(Color.WHITE)
      g.drawString("BIXUU'S ALL STORIES", x + 25, top + 59)
      g.setFont(new Font("SansSerif", Font.PLAIN, 11)); g.setColor(new Color(218, 231, 255))
      g.drawString("HINDI + ENGLISH MIX  ?  FANTASY  ?  HISTORY  ?  FABLES  ?  BIXUU ORIGINALS", x + 26, top + 83)
      g.drawString("Choose READ STORY ? Pennywise has Part 1?3 and 9 chapter options.", x + 26, top + 101)

      val pageCount = math.max(1, (stories.length + storyPageSize - 1) / storyPageSize)
      if (storyPage >= pageCount) storyPage = pageCount - 1
      val pageStories = visibleStories
      val gap = 14; val left = 250; val cardTop = 239
      val cw = math.max(205, (bw - gap * 2) / 3); val ch = 164
      storyBounds = new Array[java.awt.Rectangle](pageStories.length)
      pageStories.zipWithIndex.foreach { pair =>
        val story = pair._1; val i = pair._2
        val col = i % 3; val row = i / 3
        val cx = left + col * (cw + gap); val cy = cardTop + row * (ch + 14)
        val rect = new java.awt.Rectangle(cx, cy, cw, ch); storyBounds(i) = rect
        rounded(g, cx + 2, cy + 5, cw, ch, 17, new Color(0, 0, 0, 75))
        gradient(g, cx, cy, cw, ch, new Color(26, 35, 69), new Color(12, 20, 43), 17)
        g.setColor(new Color(story.accent.getRed, story.accent.getGreen, story.accent.getBlue, 120))
        g.setStroke(new BasicStroke(1.2f)); g.drawRoundRect(cx, cy, cw, ch, 17, 17)
        g.setColor(story.accent); g.fillRoundRect(cx + 13, cy + 14, 4, ch - 28, 5, 5)
        g.setFont(new Font("SansSerif", Font.BOLD, 8)); g.setColor(story.accent); g.drawString(story.kind, cx + 27, cy + 28)
        g.setFont(new Font("SansSerif", Font.BOLD, 12)); g.setColor(Color.WHITE)
        val title = if (story.title.length > 29) story.title.substring(0, 27) + "?" else story.title
        g.drawString(title, cx + 27, cy + 55)
        g.setFont(new Font("SansSerif", Font.PLAIN, 9)); g.setColor(new Color(177, 193, 224))
        val desc = if (story.description.length > 68) story.description.substring(0, 65) + "..." else story.description
        g.drawString(desc, cx + 27, cy + 78)
        val subline = storyHindiLine(story.id)
        g.drawString(subline, cx + 27, cy + 96)
        val by = cy + 116
        gradient(g, cx + 27, by, 112, 30, new Color(story.accent.getRed, story.accent.getGreen, story.accent.getBlue), new Color(78, 135, 211), 10)
        g.setFont(new Font("SansSerif", Font.BOLD, 9)); g.setColor(new Color(7, 17, 39))
        drawCentered(g, "READ STORY  ?", cx + 27, by + 19, 112)
      }

      val controlsY = h - 66
      storyPrevBounds = new java.awt.Rectangle(252, controlsY, 118, 30)
      storyNextBounds = new java.awt.Rectangle(386, controlsY, 118, 30)
      gradient(g, storyPrevBounds.x, storyPrevBounds.y, storyPrevBounds.width, storyPrevBounds.height,
        new Color(49, 75, 127), new Color(30, 48, 88), 10)
      gradient(g, storyNextBounds.x, storyNextBounds.y, storyNextBounds.width, storyNextBounds.height,
        new Color(49, 125, 145), new Color(30, 72, 102), 10)
      g.setFont(new Font("SansSerif", Font.BOLD, 9)); g.setColor(Color.WHITE)
      drawCentered(g, "? PREVIOUS", storyPrevBounds.x, storyPrevBounds.y + 20, storyPrevBounds.width)
      drawCentered(g, "NEXT STORIES ?", storyNextBounds.x, storyNextBounds.y + 20, storyNextBounds.width)
      g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(new Color(155, 207, 235))
      drawCentered(g, "PAGE " + (storyPage + 1) + " / " + pageCount + "     ?     " + stories.length + " STORIES", 520, controlsY + 19, math.max(200, w - 560))
      g.setFont(new Font("SansSerif", Font.PLAIN, 9)); g.setColor(new Color(130, 151, 190))
      g.drawString("Copyrighted titles contain original summaries, not full books or film scripts. Horror guide is non-graphic.", 252, h - 26)
    }

    private def storyHinglishSummary(id: String): String = id match {
      case "harry" => "Hindi + English: Harry ek magical world discover karta hai. Hogwarts mein woh friends banata hai, new skills seekhta hai aur courage, loyalty aur kindness ki importance samajhta hai."
      case "arjun" => "Hindi + English: Arjun ek focused archer hai. Mahabharata ki journey mein woh duty, discipline aur right decision lene ki value seekhta hai."
      case "monalisa" => "Hindi + English: Mona Lisa Leonardo da Vinci ki famous painting hai. Uski mysterious smile aur art technique aaj bhi people ko curious banati hai."
      case "titanic" => "Hindi + English: Titanic 1912 mein apni maiden voyage par nikla tha. Is historical tragedy ko respect ke saath yaad karte hain; isse ship safety ke important lessons mile."
      case "it" => "Hindi + English: IT ek fictional mystery-horror story hai. Derry ke friends, Pennywise ka mystery-role aur friendship ke themes is guide mein non-graphic way mein samjhaye gaye hain."
      case "bixuu" => "Hindi + English: Bixuu ko teen glowing doors milte hain?Gaming, Stories aur Robot Lab. Teamwork se woh teeno worlds ko connect karta hai."
      case "alice" => "Hindi + English: Alice ek curious journey par nikalti hai. Wonderland mein strange characters aur playful riddles uski imagination ko grow karte hain."
      case "sherlock" => "Hindi + English: Sherlock clues observe karta hai aur patience se mystery solve karta hai. Small details kabhi-kabhi biggest answer deti hain."
      case "robin" => "Hindi + English: Robin Hood apni skill aur fairness se village ki help karta hai. Real courage ka matlab dusron ke liye stand lena bhi hai."
      case "panchatantra" => "Hindi + English: Clever rabbit strength se nahi, smart thinking se problem solve karta hai. Buddhi aur courage dono important hain."
      case "tenali" => "Hindi + English: Tenali Raman apni wit se fair solution dhoondta hai. Calm mind aur smart answer arguments ko solve kar sakte hain."
      case "birbal" => "Hindi + English: Birbal ek simple example se king ka puzzle solve karta hai. New point of view se problem easy lag sakti hai."
      case "savitri" => "Hindi + English: Savitri courage, wisdom aur determination dikhati hai. Story patience aur thoughtful choices ki value batati hai."
      case "lionmouse" => "Hindi + English: Chhota mouse lion ki help karta hai. Kindness kabhi waste nahi jaati; anyone can make a difference."
      case "hare" => "Hindi + English: Tortoise slowly but steadily race complete karta hai. Consistency aur patience, overconfidence se better hain."
      case "cinderella" => "Hindi + English: Cinderella difficulties ke baad bhi kind rehti hai. Hope aur kindness uski story ka heart hain."
      case "starquest" => "Hindi + English: Bixuu ek lost star ko home find karne mein help karta hai. Small help bhi universe ko brighter bana sakti hai."
      case "robotlibrary" => "Hindi + English: Rio robot lost pages collect karne ke liye friends ke saath team banata hai. Sharing se library complete hoti hai."
      case "clockwork" => "Hindi + English: Bixuu clockwork city mein gear puzzles solve karta hai. Har small gear teamwork se city ko restart karta hai."
      case "oceans" => "Hindi + English: Bixuu beach clean-up organise karta hai aur ocean ko protect karne ka promise karta hai. Nature care sabki responsibility hai."
      case "mountain" => "Hindi + English: Seven lights ke clues friends milkar solve karte hain. Teamwork se hidden garden discover hota hai."
      case _ => "Hindi + English: Is story mein curiosity, kindness aur teamwork ka message hai."
    }

    private def showPennywiseChapterReader(): Unit = {
      val dialog = new javax.swing.JDialog(frame, "BIXUU READER ? IT / PENNYWISE", true)
      val root = new JPanel(new java.awt.BorderLayout(10, 10))
      root.setBackground(new Color(13, 18, 38))
      root.setBorder(javax.swing.BorderFactory.createEmptyBorder(14, 16, 14, 16))
      val heading = new javax.swing.JLabel("PENNYWISE STORY GUIDE  |  HINDI + ENGLISH MIX")
      heading.setForeground(new Color(255, 166, 190))
      heading.setFont(new Font("SansSerif", Font.BOLD, 17))
      root.add(heading, java.awt.BorderLayout.NORTH)

      val listPanel = new JPanel()
      listPanel.setLayout(new javax.swing.BoxLayout(listPanel, javax.swing.BoxLayout.Y_AXIS))
      listPanel.setBackground(new Color(13, 18, 38))
      var lastPart = ""
      pennywiseChapters.foreach { chapter =>
        if (chapter.part != lastPart) {
          val partLabel = new javax.swing.JLabel(chapter.part)
          partLabel.setForeground(new Color(121, 225, 245))
          partLabel.setFont(new Font("SansSerif", Font.BOLD, 12))
          partLabel.setBorder(javax.swing.BorderFactory.createEmptyBorder(12, 8, 5, 8))
          listPanel.add(partLabel)
          lastPart = chapter.part
        }
        val chapterButton = new javax.swing.JButton(chapter.title + "   ?   READ CHAPTER ?")
        chapterButton.setFont(new Font("SansSerif", Font.BOLD, 12))
        chapterButton.setForeground(new Color(243, 246, 255))
        chapterButton.setBackground(new Color(37, 48, 82))
        chapterButton.setFocusPainted(false)
        chapterButton.setBorder(javax.swing.BorderFactory.createCompoundBorder(
          javax.swing.BorderFactory.createLineBorder(new Color(94, 120, 174)),
          javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10)))
        chapterButton.addActionListener(new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = {
            val text = chapter.title + "\n\n" + chapter.summary +
              "\n\nNOTE: Original short guide summary; not copied from the novel or movie script."
            val area = new javax.swing.JTextArea(text)
            area.setEditable(false); area.setLineWrap(true); area.setWrapStyleWord(true)
            area.setFont(new Font("SansSerif", Font.PLAIN, 15))
            area.setForeground(new Color(238, 242, 255)); area.setBackground(new Color(15, 22, 43))
            area.setBorder(javax.swing.BorderFactory.createEmptyBorder(15, 16, 15, 16))
            val scroll = new javax.swing.JScrollPane(area); scroll.setPreferredSize(new Dimension(590, 260))
            javax.swing.JOptionPane.showMessageDialog(dialog, scroll, chapter.title, javax.swing.JOptionPane.INFORMATION_MESSAGE)
          }
        })
        val row = new JPanel(new java.awt.BorderLayout())
        row.setOpaque(false); row.setBorder(javax.swing.BorderFactory.createEmptyBorder(2, 4, 2, 4))
        row.add(chapterButton, java.awt.BorderLayout.CENTER)
        listPanel.add(row)
      }
      val scroll = new javax.swing.JScrollPane(listPanel)
      scroll.setBorder(javax.swing.BorderFactory.createLineBorder(new Color(55, 75, 119)))
      root.add(scroll, java.awt.BorderLayout.CENTER)
      val bottom = new javax.swing.JLabel("9 SHORT CHAPTERS  ?  3 PARTS  ?  NON-GRAPHIC STORY GUIDE")
      bottom.setForeground(new Color(164, 184, 220)); bottom.setFont(new Font("SansSerif", Font.PLAIN, 10))
      root.add(bottom, java.awt.BorderLayout.SOUTH)
      dialog.setContentPane(root); dialog.setSize(760, 650); dialog.setLocationRelativeTo(frame); dialog.setVisible(true)
    }

    private def openStory(story: StoryInfo): Unit = {
      if (story.id == "it") {
        showPennywiseChapterReader()
      } else {
        val fullText = story.body + "\n\n--------------------------------\nHINDI + ENGLISH MIX SUMMARY\n" + storyHinglishSummary(story.id)
        val area = new javax.swing.JTextArea(fullText)
        area.setEditable(false)
        area.setLineWrap(true)
        area.setWrapStyleWord(true)
        area.setFont(new Font("SansSerif", Font.PLAIN, 15))
        area.setForeground(new Color(232, 239, 255))
        area.setBackground(new Color(15, 22, 43))
        area.setBorder(javax.swing.BorderFactory.createEmptyBorder(16, 18, 16, 18))
        val scroll = new javax.swing.JScrollPane(area)
        scroll.setPreferredSize(new Dimension(610, 390))
        javax.swing.JOptionPane.showMessageDialog(frame, scroll,
          story.title + "  |  BIXUU READER", javax.swing.JOptionPane.INFORMATION_MESSAGE)
      }
    }

    private def drawRobotPage(g: Graphics2D, w: Int, h: Int): Unit = {
      val x = 250; val y = 105; val bw = w - 286
      gradient(g, x, y, bw, 126, new Color(18, 61, 98), new Color(58, 32, 110), 22)
      g.setColor(new Color(110, 225, 255, 100)); g.drawRoundRect(x, y, bw, 126, 22, 22)
      g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(new Color(126, 246, 221))
      g.drawString("BIXUU ROBOT SYSTEM", x + 25, y + 30)
      g.setFont(new Font("SansSerif", Font.BOLD, 25)); g.setColor(Color.WHITE)
      g.drawString("YOUR CREATIVE ROBOT LAB", x + 25, y + 66)
      g.setFont(new Font("SansSerif", Font.PLAIN, 11)); g.setColor(new Color(214, 228, 255))
      g.drawString("A built-in offline helper for game ideas, story discovery and code-module navigation.", x + 26, y + 91)
      g.drawString("This helper uses local preset responses; it is not a live online AI service.", x + 26, y + 110)

      val labels = Vector(
        ("BIXUU ROBOT ASSISTANT", "Ask for help with games, stories or controls.", new Color(83, 220, 218), "ASSIST"),
        ("GAME CODE LIBRARY", "Browse the six integrated game modules and launch one.", new Color(125, 169, 255), "CODE"),
        ("MISSION IDEA BUILDER", "Create a quick mission title and objective preview.", new Color(255, 190, 108), "BUILD")
      )
      val gap = 16; val left = 250; val cy = 260; val cw = math.max(210, (bw - gap * 2) / 3); val ch = 205
      robotBounds = new Array[java.awt.Rectangle](labels.length)
      labels.zipWithIndex.foreach { pair =>
        val item = pair._1; val i = pair._2; val cx = left + i * (cw + gap)
        val rect = new java.awt.Rectangle(cx, cy, cw, ch); robotBounds(i) = rect
        gradient(g, cx, cy, cw, ch, new Color(25, 35, 70), new Color(12, 19, 41), 18)
        g.setColor(new Color(item._3.getRed, item._3.getGreen, item._3.getBlue, 140)); g.drawRoundRect(cx, cy, cw, ch, 18, 18)
        g.setColor(new Color(item._3.getRed, item._3.getGreen, item._3.getBlue, 32)); g.fillOval(cx + cw - 100, cy + 15, 75, 75)
        g.setFont(new Font("SansSerif", Font.BOLD, 9)); g.setColor(item._3); g.drawString(item._4, cx + 22, cy + 34)
        g.setFont(new Font("SansSerif", Font.BOLD, 14)); g.setColor(Color.WHITE); g.drawString(item._1, cx + 22, cy + 75)
        g.setFont(new Font("SansSerif", Font.PLAIN, 10)); g.setColor(new Color(177, 194, 226))
        g.drawString(item._2, cx + 22, cy + 101)
        val btn = new java.awt.Rectangle(cx + 22, cy + 137, 142, 36)
        gradient(g, btn.x, btn.y, btn.width, btn.height, item._3, new Color(81, 135, 215), 11)
        g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(new Color(8, 18, 39))
        drawCentered(g, "OPEN  ?", btn.x, btn.y + 23, btn.width)
      }
    }

    private def runRobotAssistant(): Unit = {
      val question = javax.swing.JOptionPane.showInputDialog(frame,
        "Ask Bixuu Robot about games, stories, code or controls:", "BIXUU ROBOT ASSISTANT",
        javax.swing.JOptionPane.QUESTION_MESSAGE)
      if (question != null && question.trim.nonEmpty) {
        val q = question.toLowerCase
        val answer = if (q.contains("story") || q.contains("harry") || q.contains("arjun") || q.contains("titanic") || q.contains("monalisa") || q.contains("pennywise"))
          "Open STORY READING to browse the full Bixuu collection: Harry Potter guide, Arjun, Mona Lisa, Titanic, IT/Pennywise guide, Alice, Sherlock Holmes, Robin Hood, Panchatantra, Tenali Raman, Akbar-Birbal, Savitri, Aesop fables and original Bixuu adventures. Use PREVIOUS and NEXT STORIES to browse pages."
        else if (q.contains("code") || q.contains("scala") || q.contains("kojo"))
          "Open GAME CODE LIBRARY to view the six game modules. Select a module to launch it. For a compiler error, share the exact first error and line number."
        else if (q.contains("control") || q.contains("move"))
          "Most adventure games use W/A/S/D or arrow keys. Each game's own screen or README shows its special keys."
        else
          "Welcome to Bixuu! Choose ALL GAMES to play, STORY READING to read, or GAME CODE LIBRARY to explore the integrated modules."
        javax.swing.JOptionPane.showMessageDialog(frame, answer, "BIXUU ROBOT REPLY", javax.swing.JOptionPane.INFORMATION_MESSAGE)
      }
    }

    private def openCodeLibrary(): Unit = {
      val options: Array[Object] = Array[Object](
        "Spring Season ? Sanctus Eternal Bloom", "Sanctus Fourth Event ? 3D Advanced",
        "Sanctus Combat Arena ? Multiplayer", "Cyber Bloom ? Glitch Garden",
        "Diwali Train Adventure", "Anime Fighting Cards", "Cancel")
      val selected = javax.swing.JOptionPane.showInputDialog(frame,
        "Choose an integrated game module to open:", "BIXUU GAME CODE LIBRARY",
        javax.swing.JOptionPane.QUESTION_MESSAGE, null, options, options(0))
      if (selected != null) {
        val id = selected.toString match {
          case "Spring Season ? Sanctus Eternal Bloom" => "spring"
          case "Sanctus Fourth Event ? 3D Advanced" => "sanctus3d"
          case "Sanctus Combat Arena ? Multiplayer" => "sanctusmp"
          case "Cyber Bloom ? Glitch Garden" => "cyber"
          case "Diwali Train Adventure" => "diwali"
          case "Anime Fighting Cards" => "cards"
          case _ => ""
        }
        if (id.nonEmpty) launchGame(id)
      }
    }

    private def buildMissionIdea(): Unit = {
      val title = javax.swing.JOptionPane.showInputDialog(frame, "Mission title:", "BIXUU MISSION BUILDER", javax.swing.JOptionPane.QUESTION_MESSAGE)
      if (title != null && title.trim.nonEmpty) {
        val goal = javax.swing.JOptionPane.showInputDialog(frame, "Mission objective:", "BIXUU MISSION BUILDER", javax.swing.JOptionPane.QUESTION_MESSAGE)
        if (goal != null && goal.trim.nonEmpty) {
          javax.swing.JOptionPane.showMessageDialog(frame,
            "MISSION PREVIEW\n\n" + title.trim + "\n\nOBJECTIVE\n" + goal.trim +
              "\n\nSuggested systems: player movement ? collectibles ? timer ? clear goal ? victory screen.",
            "BIXUU ROBOT ? MISSION PREVIEW", javax.swing.JOptionPane.INFORMATION_MESSAGE)
        }
      }
    }

    private def drawFooter(g: Graphics2D, w: Int, h: Int): Unit = {
      g.setFont(new Font("SansSerif", Font.PLAIN, 9)); g.setColor(new Color(104, 125, 170))
      val tip = if (currentPage == "STORIES") "TIP: READ STORY opens the reader  ?  PREVIOUS / NEXT browse the library  ?  ESC for Home"
        else if (currentPage == "ROBOT") "TIP: Open the robot assistant, code library or mission idea builder"
        else "TIP: Press 1?6 to launch a game  ?  F to favourite the hovered card  ?  ESC to return home"
      g.drawString(tip, 252, h - 18)
      g.setFont(new Font("SansSerif", Font.BOLD, 9)); g.setColor(new Color(91, 212, 217)); g.drawString("BIXUU / 2026", w - 120, h - 18)
    }

    private def calculateHover(): Unit = {
      hoverCard = -1; hoverPlay = -1; hoverStar = -1
      var i = 0
      while (i < cardBounds.length) {
        if (cardBounds(i) != null && cardBounds(i).contains(mouseX, mouseY)) hoverCard = i
        if (playBounds.lift(i).headOption.exists(r => r != null && r.contains(mouseX, mouseY))) hoverPlay = i
        if (starBounds.lift(i).headOption.exists(r => r != null && r.contains(mouseX, mouseY))) hoverStar = i
        i += 1
      }
    }

    private def handleClick(x: Int, y: Int): Unit = {
      if (searchBounds.contains(x, y)) { searchFocused = true; requestFocusInWindow(); repaint(); return }
      searchFocused = false
      var i = 0
      while (i < navBounds.length) {
        if (navBounds(i).contains(x, y)) {
          currentPage = i match {
            case 0 => "HOME"
            case 1 => "ALL GAMES"
            case 2 => "FAVOURITES"
            case 3 => { storyPage = 0; "STORIES" }
            case 4 => "ROBOT"
            case _ => "HOME"
          }
          selectedCategory = "ALL"; searchFocused = false; repaint(); return
        }
        i += 1
      }
      if (currentPage == "STORIES") {
        if (storyPrevBounds.contains(x, y)) {
          storyPage = math.max(0, storyPage - 1); repaint(); return
        }
        val maxPage = math.max(0, (stories.length - 1) / storyPageSize)
        if (storyNextBounds.contains(x, y)) {
          storyPage = math.min(maxPage, storyPage + 1); repaint(); return
        }
        val pageStories = visibleStories
        i = 0
        while (i < storyBounds.length && i < pageStories.length) {
          if (storyBounds(i) != null && storyBounds(i).contains(x, y)) { openStory(pageStories(i)); return }
          i += 1
        }
        return
      }
      if (currentPage == "ROBOT") {
        i = 0
        while (i < robotBounds.length) {
          if (robotBounds(i) != null && robotBounds(i).contains(x, y)) {
            i match {
              case 0 => runRobotAssistant()
              case 1 => openCodeLibrary()
              case 2 => buildMissionIdea()
              case _ => ()
            }
            repaint(); return
          }
          i += 1
        }
        return
      }
      i = 0
      while (i < categoryBounds.length) {
        if (categoryBounds(i).contains(x, y)) {
          selectedCategory = Vector("ALL", "ADVENTURE", "ACTION", "FIGHTING", "FESTIVAL")(i)
          repaint(); return
        }
        i += 1
      }
      if (heroPlayBounds.contains(x, y)) { launchGame("spring"); return }
      val list = visibleGames
      i = 0
      while (i < cardBounds.length && i < list.length) {
        if (starBounds.lift(i).headOption.exists(r => r != null && r.contains(x, y))) {
          toggleFavorite(list(i).id); repaint(); return
        }
        if (playBounds.lift(i).headOption.exists(r => r != null && r.contains(x, y))) {
          launchGame(list(i).id); return
        }
        if (cardBounds(i) != null && cardBounds(i).contains(x, y)) {
          launchGame(list(i).id); return
        }
        i += 1
      }
      repaint()
    }
  }
}


// ===== INTEGRATED GAME MODULE: Bixuu_Spring_Season_Sanctus_Eternal_Bloom.scala =====


import java.awt.{AlphaComposite, BasicStroke, BorderLayout, CardLayout, Color, Component, Dimension, Font, Graphics, Graphics2D, GradientPaint, GridBagConstraints, GridBagLayout, Insets, RenderingHints}
import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, MouseAdapter, MouseEvent}
import java.io.{BufferedReader, InputStreamReader, PrintWriter}
import java.net.{InetAddress, InetSocketAddress, ServerSocket, Socket}
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import javax.swing.{BorderFactory, Box, BoxLayout, JButton, JComboBox, JComponent, JLabel, JOptionPane, JPanel, JPasswordField, JScrollPane, JTextArea, JTextField, SwingConstants, SwingUtilities, Timer, WindowConstants, JFrame}
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer

object BixuuSpringSeasonMultiplayer {

  val APP_TITLE = "BIXUU - SPRING SEASON | SANCTUS ETERNAL BLOOM"
  val ADMIN_NAME = "BIXUU@GC"
  // Keep the existing password unchanged. It is intentionally never printed in the UI.
  private val ADMIN_PIN = "BIXUU-2026"
  val DEFAULT_PORT = 5050

  val WORLD_W = 1800
  val WORLD_H = 1250
  val SCREEN_W = 1180
  val SCREEN_H = 760

  val BG_TOP = new Color(8, 15, 43)
  val BG_BOTTOM = new Color(13, 62, 69)
  val WHITE = new Color(244, 251, 255)
  val MUTED = new Color(177, 203, 216)
  val CYAN = new Color(73, 239, 235)
  val MINT = new Color(137, 255, 195)
  val PINK = new Color(255, 122, 202)
  val PURPLE = new Color(174, 119, 255)
  val GOLD = new Color(255, 224, 130)

  final class Crystal(val x: Double, val y: Double, val hue: Int, val label: String) {
    var collected = false
  }

  final class Tree(val x: Double, val y: Double, val scale: Double, val hue: Int)
  final class Flower(val x: Double, val y: Double, val size: Double, val hue: Int, val phase: Double)
  final class Star(val x: Double, val y: Double, val size: Int, val phase: Double)
  final class Particle(var x: Double, var y: Double, var vx: Double, var vy: Double, val size: Double, val hue: Int, var life: Double)

  final class Villain(val baseX: Double, val baseY: Double, val range: Double, val speed: Double, val phase: Double, val hue: Int) {
    var x = baseX
    var y = baseY
  }

  // Event 03 traversal points. Press Q near either end to swing across the vine.
  final class SwingVine(val x1: Double, val y1: Double, val x2: Double, val y2: Double, val hue: Int, val label: String)

  final case class LevelInfo(name: String, subtitle: String, crystalCount: Int, villainCount: Int, skyHue: Int)

  val levels = Map(
    1 -> LevelInfo("WHISPERING WOODS", "Find the lost nature seeds", 5, 1, 155),
    2 -> LevelInfo("AURORA FALLS", "Restore the cosmic waterfall", 6, 2, 190),
    3 -> LevelInfo("CELESTIAL WATERFALL TEMPLE", "Unlock the ancient spring portal", 7, 3, 270),
    4 -> LevelInfo("SANCTUS: ETERNAL BLOOM", "Collect wing shards, swing across vineways and awaken the Sanctus Heart", 10, 4, 148)
  )

  val completedLevels = mutable.Set[Int]()
  val extraEvents = ArrayBuffer[String]()
  val extraEvents03 = ArrayBuffer[String]()
  var activeEventTitle = "AURORA FALLS: THE LOST SPRING"
  var event03Title = "SANCTUS: ETERNAL BLOOM"
  var event03Difficulty = "Medium"
  var event03Mode = "CLASSIC"
  var activeDifficulty = "Medium"
  var activeGameMode = "CLASSIC"

  var currentUser = "Player"
  var isAdmin = false
  var activeLevel = 1
  var multiplayerMode = "SOLO"

  val crystals = ArrayBuffer[Crystal]()
  val trees = ArrayBuffer[Tree]()
  val flowers = ArrayBuffer[Flower]()
  val stars = ArrayBuffer[Star]()
  val particles = ArrayBuffer[Particle]()
  val villains = ArrayBuffer[Villain]()
  val swingVines = ArrayBuffer[SwingVine]()

  val random = new java.util.Random(20261031L)
  val keys = mutable.Set[Int]()

  var playerX = 150.0
  var playerY = 1080.0
  var facingX = 0.0
  var facingY = -1.0
  var cameraX = 0.0
  var cameraY = 0.0
  var frames = 0L
  var remainingSeconds = 360.0
  var health = 3
  var started = false
  var paused = false
  var won = false
  var lost = false
  var glowMode = true
  var showMap = true
  var dashTime = 0.0
  var dashCooldown = 0.0
  var hitCooldown = 0.0
  var wingCharges = 0
  var wingBoostTime = 0.0
  var swingCooldown = 0.0
  var toast = "Explore the world and find the magical crystals."
  var toastTime = 4.0

  val portalX = 1630.0
  val portalY = 190.0
  val spawnX = 150.0
  val spawnY = 1080.0

  var frame: JFrame = null
  var root: JPanel = null
  var cards: CardLayout = null
  var gamePanel: GamePanel = null
  var gameTimer: Timer = null

  var nameField: JTextField = null
  var pinField: JPasswordField = null
  var adminButton: JButton = null
  var levelButtons = ArrayBuffer[JButton]()
  var hostIpLabel: JLabel = null
  var multiplayerStatus: JLabel = null
  var eventTitleField: JTextField = null
  var event03TitleField: JTextField = null
  var adminDifficultyBox: JComboBox[String] = null
  var adminModeBox: JComboBox[String] = null
  var level04Heading: JLabel = null
  var level04Details: JLabel = null
  var eventListArea: JTextArea = null
  var welcomeLabel: JLabel = null

  var lanServer: Option[LanServer] = None
  var lanClient: Option[LanClient] = None

  // ============================================================
  // COMMON HELPERS
  // ============================================================

  def clamp(v: Double, lo: Double, hi: Double): Double =
    math.max(lo, math.min(hi, v))

  def dist(x1: Double, y1: Double, x2: Double, y2: Double): Double = {
    val dx = x1 - x2
    val dy = y1 - y2
    math.sqrt(dx * dx + dy * dy)
  }

  def hueColor(h: Int, s: Float = 0.78f, b: Float = 1.0f): Color =
    Color.getHSBColor((h.toDouble / 360.0).toFloat, s, b)

  def safeName(s: String): String = {
    val cleaned = Option(s).getOrElse("").trim.replaceAll("[,;|\\r\\n]", "_")
    if (cleaned.isEmpty) "Player" else cleaned.take(18)
  }

  def showToast(s: String, seconds: Double = 2.8): Unit = {
    toast = s
    toastTime = seconds
  }

  def eventNameForLevel(level: Int): String = level match {
    case 1 => "EVENT 01: GLITCH IN THE GARDEN"
    case 2 | 3 => "EVENT 02: " + activeEventTitle
    case 4 => "EVENT 03: " + event03Title
    case _ => "SPRING SEASON ADVENTURE"
  }

  def difficultyVillainBonus(name: String): Int = name match {
    case "Easy" => -1
    case "Medium" => 0
    case "Hard" => 1
    case "Expert" => 2
    case "Celestial" => 3
    case _ => 0
  }

  def difficultySpeed(name: String): Double = name match {
    case "Easy" => 0.82
    case "Medium" => 1.0
    case "Hard" => 1.12
    case "Expert" => 1.25
    case "Celestial" => 1.40
    case _ => 1.0
  }

  def collectedCount: Int = crystals.count(_.collected)
  def sx(x: Double): Int = math.round(x - cameraX).toInt
  def sy(y: Double): Int = math.round(y - cameraY).toInt

  def styledButton(text: String): JButton = {
    val b = new JButton(text)
    b.setFont(new Font("SansSerif", Font.BOLD, 15))
    b.setForeground(WHITE)
    b.setBackground(new Color(24, 99, 91))
    b.setFocusPainted(false)
    b.setBorder(BorderFactory.createCompoundBorder(
      BorderFactory.createLineBorder(new Color(82, 220, 208), 1),
      BorderFactory.createEmptyBorder(10, 18, 10, 18)
    ))
    b.setAlignmentX(Component.CENTER_ALIGNMENT)
    b
  }

  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, 14))
    l.setForeground(MUTED)
    l.setAlignmentX(Component.CENTER_ALIGNMENT)
    l
  }

  def verticalPanel(): JPanel = {
    val p = new JPanel()
    p.setOpaque(true)
    p.setBackground(new Color(9, 20, 42))
    p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS))
    p.setBorder(BorderFactory.createEmptyBorder(24, 35, 24, 35))
    p
  }

  // FIX 1: JComponent, not java.awt.Component.
  def addCentered(p: JPanel, c: JComponent, gap: Int = 10): Unit = {
    p.add(Box.createVerticalStrut(gap))
    c.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(c)
  }

  def showCard(name: String): Unit = {
    cards.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
    })
  }

  // ============================================================
  // LAN MULTIPLAYER SERVER
  // ============================================================

  final class LanServer(port: Int, hostName: String) {

    private val serverSocket = new ServerSocket()
    private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
    private val counter = new AtomicInteger(0)

    private val positions = new ConcurrentHashMap[String, String]()
    private val writers = new ConcurrentHashMap[String, PrintWriter]()
    private val sharedCrystals = new ConcurrentHashMap[String, String]()

    serverSocket.setReuseAddress(true)
    serverSocket.bind(new InetSocketAddress(port))

    def start(): Unit = {
      positions.put("HOST-" + safeName(hostName), "150.0,1080.0")

      val t = new Thread(new Runnable {
        override def run(): Unit = {
          while (running.get()) {
            try {
              val socket = serverSocket.accept()
              socket.setTcpNoDelay(true)

              val worker = new Thread(new Runnable {
                override def run(): Unit = handleClient(socket)
              })

              worker.setDaemon(true)
              worker.start()

            } catch {
              case _: java.net.SocketException => ()
              case _: Throwable => ()
            }
          }
        }
      })

      t.setDaemon(true)
      t.start()
    }

    private def handleClient(socket: Socket): Unit = {

      var id = ""
      var out: PrintWriter = null

      try {
        val in = new BufferedReader(
          new InputStreamReader(socket.getInputStream, "UTF-8")
        )

        out = new PrintWriter(socket.getOutputStream, true)

        val first = in.readLine()

        if (first != null && first.startsWith("JOIN|")) {

          val requested = safeName(first.substring(5))
          id = requested + "-" + counter.incrementAndGet()

          writers.put(id, out)
          positions.put(id, "150.0,1080.0")

          var line = in.readLine()

          while (line != null && running.get()) {

            if (line.startsWith("POS|")) {

              val coords = line.substring(4).split(",", 2)

              if (coords.length == 2) {
                try {
                  val px = clamp(coords(0).toDouble, 0.0, WORLD_W.toDouble)
                  val py = clamp(coords(1).toDouble, 0.0, WORLD_H.toDouble)
                  positions.put(id, px.toString + "," + py.toString)
                } catch {
                  case _: NumberFormatException => ()
                }
              }

            } else if (line.startsWith("COLLECT|")) {

              try {
                val index = line.substring(8).trim.toInt
                if (index >= 0 && index < 10) {
                  sharedCrystals.put(index.toString, "1")
                }
              } catch {
                case _: NumberFormatException => ()
              }
            }

            line = in.readLine()
          }
        }

      } catch {
        case _: Throwable => ()
      } finally {

        if (id.nonEmpty) {
          writers.remove(id)
          positions.remove(id)
        }

        try socket.close() catch {
          case _: Throwable => ()
        }
      }
    }

    def snapshot(): Seq[(String, String)] = {
      val result = ArrayBuffer[(String, String)]()
      val it = positions.entrySet().iterator()

      while (it.hasNext) {
        val e = it.next()
        result += ((e.getKey, e.getValue))
      }

      result.toSeq
    }

    def markCrystalCollected(index: Int): Unit = {
      if (index >= 0 && index < 10) {
        sharedCrystals.put(index.toString, "1")
      }
    }

    def collectedSnapshot(): Set[Int] = {
      val result = mutable.Set[Int]()
      val it = sharedCrystals.keySet().iterator()

      while (it.hasNext) {
        try result += it.next().toInt
        catch {
          case _: NumberFormatException => ()
        }
      }

      result.toSet
    }

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

      positions.put(
        "HOST-" + safeName(hostName),
        x.toString + "," + y.toString
      )

      val sb = new StringBuilder("STATE|")
      val it = positions.entrySet().iterator()
      var first = true

      while (it.hasNext) {
        val e = it.next()

        if (!first) sb.append(";")
        first = false

        sb.append(e.getKey).append(",").append(e.getValue)
      }

      sb.append("|")

      val gemIt = sharedCrystals.keySet().iterator()
      var firstGem = true

      while (gemIt.hasNext) {
        if (!firstGem) sb.append(",")
        firstGem = false
        sb.append(gemIt.next())
      }

      val message = sb.toString
      val wi = writers.values().iterator()

      while (wi.hasNext) {
        try wi.next().println(message)
        catch {
          case _: Throwable => ()
        }
      }
    }

    def close(): Unit = {
      running.set(false)
      try serverSocket.close()
      catch {
        case _: Throwable => ()
      }

      val wi = writers.values().iterator()

      while (wi.hasNext) {
        try wi.next().close()
        catch {
          case _: Throwable => ()
        }
      }

      writers.clear()
      positions.clear()
      sharedCrystals.clear()
    }
  }

  // ============================================================
  // LAN MULTIPLAYER CLIENT
  // ============================================================

  final class LanClient(host: String, port: Int, playerName: String) {

    private val socket = new Socket()
    socket.connect(new InetSocketAddress(host, port), 3500)
    socket.setTcpNoDelay(true)

    private val out = new PrintWriter(socket.getOutputStream, true)

    private val in = new BufferedReader(
      new InputStreamReader(socket.getInputStream, "UTF-8")
    )

    private val received = new ConcurrentHashMap[String, String]()
    private val receivedCrystals = new ConcurrentHashMap[String, String]()

    @volatile private var connected = true

    out.println("JOIN|" + safeName(playerName))

    private val readerThread = 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)

              if (sections.length >= 2) {

                val entries = sections(1).split(";")

                received.clear()

                entries.foreach { entry =>
                  val parts = entry.split(",", 3)

                  if (parts.length == 3) {
                    received.put(
                      parts(0),
                      parts(1) + "," + parts(2)
                    )
                  }
                }

                receivedCrystals.clear()

                if (sections.length == 3 && sections(2).nonEmpty) {
                  sections(2).split(",").foreach { i =>
                    receivedCrystals.put(i, "1")
                  }
                }
              }
            }

            line = in.readLine()
          }

        } catch {
          case _: Throwable => ()
        }

        connected = false
      }
    })

    readerThread.setDaemon(true)
    readerThread.start()

    def sendPosition(x: Double, y: Double): Unit = {
      if (connected) {
        out.println("POS|" + x.toString + "," + y.toString)
      }
    }

    def collectCrystal(index: Int): Unit = {
      if (connected && index >= 0 && index < 10) {
        out.println("COLLECT|" + index)
      }
    }

    def collectedSnapshot(): Set[Int] = {
      val result = mutable.Set[Int]()
      val it = receivedCrystals.keySet().iterator()

      while (it.hasNext) {
        try result += it.next().toInt
        catch {
          case _: NumberFormatException => ()
        }
      }

      result.toSet
    }

    def snapshot(): Seq[(String, String)] = {
      val b = ArrayBuffer[(String, String)]()
      val it = received.entrySet().iterator()

      while (it.hasNext) {
        val e = it.next()
        b += ((e.getKey, e.getValue))
      }

      b.toSeq
    }

    def isConnected: Boolean = connected

    def close(): Unit = {
      connected = false
      try socket.close()
      catch {
        case _: Throwable => ()
      }
    }
  }

  // ============================================================
  // GAME WORLD
  // ============================================================

  def setupLevel(level: Int): Unit = {

    if (!levels.contains(level)) {
      JOptionPane.showMessageDialog(frame, "This adventure level does not exist.")
      return
    }

    activeLevel = level
    activeDifficulty = if (level == 4) event03Difficulty else "Medium"
    activeGameMode = if (level == 4) event03Mode else "CLASSIC"

    crystals.clear()
    trees.clear()
    flowers.clear()
    stars.clear()
    particles.clear()
    villains.clear()
    swingVines.clear()
    keys.clear()
    wingCharges = 0
    wingBoostTime = 0.0
    swingCooldown = 0.0

    val crystalData = level match {

      case 1 =>
        Array(
          (350.0, 900.0, 150, "MINT SEED"),
          (620.0, 680.0, 48, "SUN PETAL"),
          (940.0, 940.0, 190, "STAR BLOOM"),
          (1250.0, 650.0, 285, "MOON GEM"),
          (1510.0, 360.0, 315, "SPRING ORB")
        )

      case 2 =>
        Array(
          (300.0, 920.0, 155, "MINT SEED"),
          (580.0, 740.0, 190, "STAR PETAL"),
          (900.0, 950.0, 285, "MOON BLOOM"),
          (1260.0, 900.0, 48, "SUN CRYSTAL"),
          (1510.0, 650.0, 195, "WATER GEM"),
          (1050.0, 350.0, 315, "AURORA ORB")
        )

      case 3 =>
        Array(
          (280.0, 900.0, 150, "FOREST KEY"),
          (540.0, 650.0, 190, "MOON PETAL"),
          (850.0, 1000.0, 285, "DREAM GEM"),
          (1150.0, 830.0, 48, "SUN KEY"),
          (1480.0, 700.0, 195, "WATER HEART"),
          (1280.0, 350.0, 315, "STAR ORB"),
          (750.0, 300.0, 265, "TEMPLE SEED")
        )

      case 4 =>
        if (activeGameMode == "CRYSTAL RUSH") {
          Array(
            (320.0, 900.0, 150, "SUNROOT SEED"),
            (560.0, 720.0, 190, "MOONLEAF"),
            (820.0, 980.0, 285, "AETHER BLOOM"),
            (1080.0, 770.0, 48, "GOLDEN HEART"),
            (1370.0, 900.0, 195, "SANCTUS CRYSTAL"),
            (1550.0, 540.0, 315, "STARFRUIT"),
            (1160.0, 330.0, 265, "ROOT KEY"),
            (720.0, 360.0, 180, "CELESTIAL SEED"),
            (430.0, 430.0, 48, "WING SHARD"),
            (1510.0, 1010.0, 315, "WING FEATHER")
          )
        } else {
          Array(
            (320.0, 900.0, 150, "SUNROOT SEED"),
            (560.0, 720.0, 190, "MOONLEAF"),
            (820.0, 980.0, 285, "AETHER BLOOM"),
            (1080.0, 770.0, 48, "GOLDEN HEART"),
            (1370.0, 900.0, 195, "SANCTUS CRYSTAL"),
            (1550.0, 540.0, 315, "STARFRUIT"),
            (1160.0, 330.0, 265, "ROOT KEY"),
            (720.0, 360.0, 180, "CELESTIAL SEED"),
            (430.0, 430.0, 48, "WING SHARD"),
            (1510.0, 1010.0, 315, "WING FEATHER")
          )
        }
    }

    crystalData.foreach { c =>
      crystals += new Crystal(c._1, c._2, c._3, c._4)
    }

    if (level == 4) {
      swingVines ++= Seq(
        new SwingVine(560.0, 800.0, 820.0, 590.0, 150, "MINT VINEWAY"),
        new SwingVine(820.0, 590.0, 1090.0, 390.0, 190, "AURORA VINEWAY"),
        new SwingVine(1090.0, 390.0, 1450.0, 320.0, 285, "CELESTIAL VINEWAY"),
        new SwingVine(1050.0, 880.0, 1400.0, 720.0, 48, "GOLDEN VINEWAY"),
        new SwingVine(1450.0, 320.0, 1600.0, 220.0, 315, "SANCTUS GATEWAY")
      )
    }

    val treeData = Array(
      (100.0, 260.0, 1.1, 150),
      (330.0, 180.0, 0.9, 160),
      (660.0, 220.0, 1.0, 145),
      (1010.0, 190.0, 0.9, 155),
      (1740.0, 230.0, 1.1, 165),
      (1770.0, 600.0, 1.0, 150),
      (1700.0, 1050.0, 1.1, 160),
      (1320.0, 1150.0, 0.9, 145),
      (850.0, 1160.0, 1.0, 165),
      (390.0, 1170.0, 1.1, 155),
      (100.0, 820.0, 0.9, 160),
      (480.0, 470.0, 0.8, 150),
      (960.0, 700.0, 0.9, 165),
      (1320.0, 330.0, 0.8, 150),
      (650.0, 1000.0, 0.85, 145)
    )

    treeData.foreach { t =>
      trees += new Tree(t._1, t._2, t._3, t._4)
    }

    val flowerZones = Array(
      (80.0, 120.0, 600.0, 340.0),
      (1150.0, 110.0, 570.0, 340.0),
      (80.0, 930.0, 620.0, 260.0),
      (1120.0, 950.0, 600.0, 250.0),
      (600.0, 400.0, 320.0, 220.0)
    )

    var i = 0

    while (i < 210) {
      val z = flowerZones(random.nextInt(flowerZones.length))

      flowers += new Flower(
        z._1 + random.nextDouble() * z._3,
        z._2 + random.nextDouble() * z._4,
        4.0 + random.nextDouble() * 8.0,
        Array(145, 164, 188, 282, 315, 48)(random.nextInt(6)),
        random.nextDouble() * 6.28
      )

      i += 1
    }

    i = 0

    while (i < 170) {
      stars += new Star(
        random.nextDouble() * WORLD_W,
        random.nextDouble() * WORLD_H,
        1 + random.nextInt(3),
        random.nextDouble() * 6.28
      )
      i += 1
    }

    i = 0

    while (i < 100) {
      particles += new Particle(
        random.nextDouble() * WORLD_W,
        random.nextDouble() * WORLD_H,
        (random.nextDouble() - 0.5) * 8.0,
        -5.0 - random.nextDouble() * 18.0,
        2.0 + random.nextDouble() * 3.0,
        Array(150, 165, 188, 285, 315, 48)(random.nextInt(6)),
        4.0 + random.nextDouble() * 4.0
      )
      i += 1
    }

    val count = math.max(
      1,
      levels(level).villainCount +
        (if (level == 4) difficultyVillainBonus(activeDifficulty) else 0)
    )
    val speedFactor = if (level == 4) difficultySpeed(activeDifficulty) else 1.0
    i = 0

    while (i < count) {
      val vx = if (level == 4) clamp(420.0 + i * 190.0, 300.0, WORLD_W - 120.0)
               else 540.0 + i * 390.0
      val vy = if (level == 4) 470.0 + (i % 3) * 250.0
               else 520.0 + (i % 2) * 300.0

      villains += new Villain(
        vx,
        vy,
        80.0 + i * 18.0,
        (0.8 + i * 0.25) * speedFactor,
        i * 1.7,
        if (i % 2 == 0) 330 else 275
      )

      i += 1
    }

    playerX = spawnX
    playerY = spawnY

    facingX = 0.0
    facingY = -1.0

    cameraX = 0.0
    cameraY = 0.0

    frames = 0L

    val baseTime = if (level == 4) {
      activeDifficulty match {
        case "Easy" => 480.0
        case "Medium" => 360.0
        case "Hard" => 300.0
        case "Expert" => 240.0
        case "Celestial" => 210.0
        case _ => 360.0
      }
    } else 360.0

    remainingSeconds = activeGameMode match {
      case "TIME ATTACK" => math.max(90.0, baseTime - 90.0)
      case "SURVIVAL" => math.max(360.0, baseTime + 120.0)
      case _ => baseTime
    }

    health = if (level == 4) {
      if (activeGameMode == "SURVIVAL") 1
      else activeDifficulty match {
        case "Easy" => 5
        case "Medium" => 3
        case "Hard" => 3
        case "Expert" => 2
        case "Celestial" => 1
        case _ => 3
      }
    } else 3

    started = true
    paused = false
    won = false
    lost = false

    dashTime = 0.0
    dashCooldown = 0.0
    hitCooldown = 0.0
    wingBoostTime = 0.0
    swingCooldown = 0.0
    wingCharges = 0

    glowMode = true
    showMap = true

    showToast(
      "Level " + level + " - " + levels(level).name +
      " | " + activeDifficulty + " | " + activeGameMode +
      ". Find every crystal!",
      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 returnToLevels(): Unit = {
    if (gameTimer != null) gameTimer.stop()
    closeNetwork()
    showCard("levels")
    refreshLevelButtons()
  }

  def restartCurrentLevel(): Unit = {
    setupLevel(activeLevel)
    if (gamePanel != null) gamePanel.requestFocusInWindow()
  }

  def closeNetwork(): Unit = {
    lanClient.foreach(_.close())
    lanClient = None

    lanServer.foreach(_.close())
    lanServer = None
  }

  // ============================================================
  // COLLECTION AND GAMEPLAY
  // ============================================================

  def collectOrUse(): Unit = {

    if (!started || paused || won || lost) return

    val nearest = crystals
      .filter(!_.collected)
      .sortBy(c => dist(playerX, playerY, c.x, c.y))
      .headOption

    nearest match {

      case Some(c) if dist(playerX, playerY, c.x, c.y) < 92.0 =>

        val crystalIndex = crystals.indexOf(c)

        c.collected = true
        if (activeLevel == 4 && c.label.toUpperCase.contains("WING")) {
          wingCharges = math.min(wingCharges + 1, 5)
        }

        if (multiplayerMode == "HOST") {
          lanServer.foreach(_.markCrystalCollected(crystalIndex))
        }

        if (multiplayerMode == "JOIN") {
          lanClient.foreach(_.collectCrystal(crystalIndex))
        }

        showToast(
          "CRYSTAL FOUND: " + c.label +
          "  [" + collectedCount + "/" + crystals.length + "]",
          2.6
        )

        spawnBurst(c.x, c.y, c.hue, 24)

        if (collectedCount == crystals.length) {
          showToast("ALL CRYSTALS FOUND! Reach the glowing portal.", 4.0)
        }

      case _ =>

        if (dist(playerX, playerY, portalX, portalY) < 120.0) {

          if (collectedCount == crystals.length) {

            won = true
            completedLevels += activeLevel

            showToast(
              "LEVEL COMPLETE! Great adventure, " +
              currentUser + "!",
              8.0
            )

            spawnBurst(portalX, portalY, 165, 85)

          } else {
            showToast(
              "PORTAL LOCKED - collect all " +
              crystals.length + " crystals first.",
              2.8
            )
          }

        } else {
          showToast(
            "Move closer to a crystal or the portal, then press E.",
            2.4
          )
        }
    }
  }

  def useSwingVine(): Unit = {
    if (activeLevel != 4 || paused || won || lost || swingCooldown > 0.0) return

    val options = swingVines.flatMap { vine =>
      Seq(
        (dist(playerX, playerY, vine.x1, vine.y1), vine.x1, vine.y1, vine.x2, vine.y2, vine),
        (dist(playerX, playerY, vine.x2, vine.y2), vine.x2, vine.y2, vine.x1, vine.y1, vine)
      )
    }.filter(_._1 < 118.0).sortBy(_._1)

    options.headOption match {
      case Some((_, fromX, fromY, toX, toY, vine)) =>
        playerX = clamp(toX + (if (toX < fromX) -24.0 else 24.0), 40.0, WORLD_W - 40.0)
        playerY = clamp(toY + (if (toY < fromY) -18.0 else 18.0), 40.0, WORLD_H - 40.0)
        facingX = if (toX < fromX) -1.0 else 1.0
        facingY = if (toY < fromY) -0.4 else 0.4
        swingCooldown = 1.15
        spawnBurst(toX, toY, vine.hue, 28)
        showToast("VINE SWING: " + vine.label, 2.0)
      case None =>
        showToast("Move close to a glowing vine anchor, then press Q to swing.", 2.2)
    }
  }

  def activateWings(): Unit = {
    if (activeLevel != 4 || paused || won || lost) return
    if (wingCharges <= 0) {
      showToast("Find a WING SHARD or WING FEATHER first.", 2.0)
    } else if (wingBoostTime <= 0.0) {
      wingCharges -= 1
      wingBoostTime = 2.1
      dashTime = 2.1
      dashCooldown = 2.1
      spawnBurst(playerX, playerY, 180, 34)
      showToast("WINGS OF DAWN ACTIVATED!", 2.2)
    }
  }

  def spawnBurst(
    x: Double,
    y: Double,
    hue: Int,
    amount: Int
  ): Unit = {

    var i = 0

    while (i < amount) {

      val a = random.nextDouble() * math.Pi * 2.0
      val speed = 25.0 + random.nextDouble() * 125.0

      particles += new Particle(
        x,
        y,
        math.cos(a) * speed,
        math.sin(a) * speed,
        2.0 + random.nextDouble() * 3.5,
        hue,
        0.8 + random.nextDouble() * 1.0
      )

      i += 1
    }
  }

  def updateGame(dt: Double): Unit = {

    frames += 1

    if (toastTime > 0.0) {
      toastTime = math.max(0.0, toastTime - dt)
    }

    if (dashTime > 0.0) {
      dashTime = math.max(0.0, dashTime - dt)
    }

    if (dashCooldown > 0.0) {
      dashCooldown = math.max(0.0, dashCooldown - dt)
    }

    if (hitCooldown > 0.0) {
      hitCooldown = math.max(0.0, hitCooldown - dt)
    }
    if (wingBoostTime > 0.0) {
      wingBoostTime = math.max(0.0, wingBoostTime - dt)
    }
    if (swingCooldown > 0.0) {
      swingCooldown = math.max(0.0, swingCooldown - dt)
    }

    particles.foreach { p =>
      p.x += p.vx * dt
      p.y += p.vy * dt
      p.life -= dt

      if (p.y < 0) p.y = WORLD_H
      if (p.x < 0) p.x = WORLD_W
      if (p.x > WORLD_W) p.x = 0
    }

    val expired = particles.filter(_.life <= 0.0)
    expired.foreach(p => particles -= p)

    while (particles.length < 90) {

      particles += new Particle(
        random.nextDouble() * WORLD_W,
        WORLD_H - 5,
        (random.nextDouble() - 0.5) * 12.0,
        -6.0 - random.nextDouble() * 17.0,
        2.0 + random.nextDouble() * 3.0,
        Array(150, 165, 188, 285, 315, 48)(random.nextInt(6)),
        4.0
      )
    }

    villains.foreach { v =>
      v.x = v.baseX + math.sin(frames * 0.025 * v.speed + v.phase) * v.range
      v.y = v.baseY + math.cos(frames * 0.018 + v.phase) * 13.0
    }

    if (!started || paused || won || lost) return

    remainingSeconds = math.max(0.0, remainingSeconds - dt)

    if (remainingSeconds <= 0.0) {
      lost = true
      showToast("TIME OVER! Press R to retry this level.", 8.0)
      return
    }

    var mx = 0.0
    var my = 0.0

    if (keys.contains(KeyEvent.VK_W) || keys.contains(KeyEvent.VK_UP)) my -= 1.0
    if (keys.contains(KeyEvent.VK_S) || keys.contains(KeyEvent.VK_DOWN)) my += 1.0
    if (keys.contains(KeyEvent.VK_A) || keys.contains(KeyEvent.VK_LEFT)) mx -= 1.0
    if (keys.contains(KeyEvent.VK_D) || keys.contains(KeyEvent.VK_RIGHT)) mx += 1.0

    val len = math.sqrt(mx * mx + my * my)

    if (len > 0.0) {
      mx /= len
      my /= len

      facingX = mx
      facingY = my

      val speed =
        if (wingBoostTime > 0.0) 520.0
        else if (dashTime > 0.0) 410.0
        else 205.0

      playerX = clamp(
        playerX + mx * speed * dt,
        40.0,
        WORLD_W - 40.0
      )

      playerY = clamp(
        playerY + my * speed * dt,
        40.0,
        WORLD_H - 40.0
      )
    }

    villains.foreach { v =>

      if (
        hitCooldown <= 0.0 &&
        dist(playerX, playerY, v.x, v.y) < 39.0 &&
        dashTime <= 0.0
      ) {

        health -= 1
        hitCooldown = 1.5

        playerX = clamp(
          playerX - facingX * 65.0,
          40.0,
          WORLD_W - 40.0
        )

        playerY = clamp(
          playerY - facingY * 65.0,
          40.0,
          WORLD_H - 40.0
        )

        showToast(
          "A garden shadow caught you! Hearts left: " + health,
          2.0
        )

        spawnBurst(playerX, playerY, 330, 12)

        if (health <= 0) {
          lost = true
          showToast("ADVENTURE OVER! Press R to retry.", 8.0)
        }
      }
    }

    multiplayerMode match {

      case "HOST" =>
        lanServer.foreach { server =>

          server.collectedSnapshot().foreach { index =>
            if (index >= 0 && index < crystals.length && !crystals(index).collected) {
              crystals(index).collected = true
              if (activeLevel == 4 && crystals(index).label.toUpperCase.contains("WING")) {
                wingCharges = math.min(wingCharges + 1, 5)
              }
            }
          }

          server.publishHostPosition(playerX, playerY)
        }

      case "JOIN" =>
        lanClient.foreach { client =>

          client.collectedSnapshot().foreach { index =>
            if (index >= 0 && index < crystals.length && !crystals(index).collected) {
              crystals(index).collected = true
              if (activeLevel == 4 && crystals(index).label.toUpperCase.contains("WING")) {
                wingCharges = math.min(wingCharges + 1, 5)
              }
            }
          }

          client.sendPosition(playerX, playerY)
        }

      case _ => ()
    }
  }

  // ============================================================
  // DRAWING
  // ============================================================

  def drawGlow(
    g: Graphics2D,
    x: Int,
    y: Int,
    r: Int,
    color: Color,
    alpha: Float
  ): Unit = {

    val old = g.getComposite

    g.setComposite(
      AlphaComposite.SrcOver.derive(alpha)
    )

    g.setColor(color)
    g.fillOval(x - r, y - r, r * 2, r * 2)

    g.setComposite(old)
  }

  def drawCentered(
    g: Graphics2D,
    text: String,
    x: Int,
    y: Int
  ): Unit = {
    g.drawString(
      text,
      x - g.getFontMetrics.stringWidth(text) / 2,
      y
    )
  }

  def drawBackground(
    g: Graphics2D,
    w: Int,
    h: Int
  ): Unit = {

    val hue = levels(activeLevel).skyHue
    val top = hueColor(hue, 0.70f, 0.20f)
    val bottom = new Color(14, 72, 66)

    g.setPaint(
      new GradientPaint(0f, 0f, top, 0f, h.toFloat, bottom)
    )

    g.fillRect(0, 0, w, h)

    stars.foreach { s =>

      val x = sx(s.x)
      val y = sy(s.y)

      if (x >= 0 && y >= 0 && x < w && y < h) {

        val a =
          110 + math.abs(
            (math.sin(frames * 0.08 + s.phase) * 130).toInt
          )

        g.setColor(new Color(255, 255, 255, a))
        g.fillOval(x, y, s.size, s.size)
      }
    }

    val moonX = sx(1500)
    val moonY = sy(140)

    drawGlow(g, moonX, moonY, 105, PURPLE, 0.12f)

    g.setColor(new Color(255, 236, 185))
    g.fillOval(moonX - 38, moonY - 38, 76, 76)
  }

  def drawGround(
    g: Graphics2D,
    w: Int,
    h: Int
  ): Unit = {

    val groundTop = (h * 0.30).toInt

    g.setPaint(
      new GradientPaint(
        0f,
        groundTop.toFloat,
        new Color(47, 145, 91),
        0f,
        h.toFloat,
        new Color(8, 48, 46)
      )
    )

    g.fillRect(0, groundTop, w, h - groundTop)

    val old = g.getComposite

    g.setComposite(
      AlphaComposite.SrcOver.derive(0.08f)
    )

    g.setColor(CYAN)

    var x = 0
    while (x < w) {
      g.drawLine(x, groundTop, x, h)
      x += 85
    }

    var y = groundTop
    while (y < h) {
      g.drawLine(0, y, w, y)
      y += 85
    }

    g.setComposite(old)
  }

  def drawTree(g: Graphics2D, t: Tree): Unit = {

    val x = sx(t.x)
    val y = sy(t.y)
    val s = t.scale

    if (x < -130 || y < -180 || x > SCREEN_W + 130 || y > SCREEN_H + 130) return

    g.setColor(new Color(79, 53, 37))

    g.fillRoundRect(
      (x - 8 * s).toInt,
      (y - 75 * s).toInt,
      (16 * s).toInt,
      (80 * s).toInt,
      8,
      8
    )

    g.setColor(hueColor(t.hue, 0.65f, 0.43f))

    g.fillOval(
      (x - 42 * s).toInt,
      (y - 144 * s).toInt,
      (84 * s).toInt,
      (86 * s).toInt
    )

    g.setColor(hueColor(t.hue, 0.58f, 0.64f))

    g.fillOval(
      (x - 31 * s).toInt,
      (y - 158 * s).toInt,
      (65 * s).toInt,
      (65 * s).toInt
    )

    g.setColor(new Color(180, 255, 213, 110))

    g.drawArc(
      (x - 26 * s).toInt,
      (y - 151 * s).toInt,
      (54 * s).toInt,
      (49 * s).toInt,
      35,
      120
    )
  }

  def drawFlower(g: Graphics2D, f: Flower): Unit = {

    val x = sx(f.x)
    val y = sy(f.y)

    if (x < -25 || y < -25 || x > SCREEN_W + 25 || y > SCREEN_H + 25) return

    val r = math.max(
      3,
      (f.size * (0.86 + 0.14 * math.sin(frames * 0.06 + f.phase))).toInt
    )

    g.setColor(new Color(35, 117, 70))
    g.drawLine(x, y + 2, x, y + r * 3)

    g.setColor(hueColor(f.hue))

    var i = 0

    while (i < 5) {
      val a = i * math.Pi * 2.0 / 5.0
      g.fillOval(
        x + (math.cos(a) * r * 0.7).toInt - r / 2,
        y + (math.sin(a) * r * 0.7).toInt - r / 2,
        r,
        r
      )
      i += 1
    }

    g.setColor(GOLD)
    g.fillOval(x - 3, y - 3, 6, 6)
  }

  def drawWaterfall(g: Graphics2D): Unit = {

    val x = sx(1510)
    val y = sy(170)

    g.setColor(new Color(70, 91, 91))
    g.fillRoundRect(x - 120, y - 25, 240, 440, 30, 30)

    g.setPaint(
      new GradientPaint(
        x - 45,
        y.toFloat,
        new Color(192, 250, 255),
        x + 55,
        (y + 390).toFloat,
        new Color(17, 116, 219)
      )
    )

    g.fillRoundRect(x - 52, y + 25, 104, 360, 22, 22)

    for (i <- 0 until 15) {

      val wx = x - 42 + i * 6
      val wy = y + 35 + ((frames * 6 + i * 27) % 320).toInt

      g.setColor(new Color(238, 255, 255, 190))
      g.fillRoundRect(wx, wy, 3, 29, 5, 5)
    }

    g.setColor(new Color(30, 166, 220, 230))
    g.fillOval(x - 150, y + 350, 300, 88)

    g.setColor(new Color(180, 250, 255, 150))
    g.drawOval(x - 90, y + 370, 180, 35)
    g.drawOval(x - 120, y + 382, 240, 45)
  }

  // Event 03 landmark: layered roots, a luminous canopy and a floating altar.
  def drawSanctusSanctuary(g: Graphics2D): Unit = {
    val x = sx(1450)
    val y = sy(355)
    val pulse = 0.5 + 0.5 * math.sin(frames * 0.045)

    drawGlow(g, x, y - 105, 210, new Color(115, 255, 190), 0.10f + pulse.toFloat * 0.04f)
    drawGlow(g, x, y + 145, 145, new Color(85, 220, 255), 0.11f)

    // Ancient trunk and layered roots.
    g.setPaint(new GradientPaint(x - 55, y - 90, new Color(116, 91, 55),
      x + 65, y + 300, new Color(31, 87, 65)))
    g.fillRoundRect(x - 58, y - 95, 116, 405, 34, 34)
    g.setColor(new Color(194, 170, 105, 180))
    g.setStroke(new BasicStroke(5.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
    g.drawLine(x - 22, y + 250, x - 175, y + 345)
    g.drawLine(x + 22, y + 250, x + 170, y + 345)
    g.drawLine(x - 5, y + 280, x - 88, y + 380)
    g.drawLine(x + 8, y + 280, x + 85, y + 380)

    // Branches reaching into the sky.
    g.setStroke(new BasicStroke(18.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
    g.setColor(new Color(95, 130, 75))
    g.drawLine(x - 20, y - 35, x - 165, y - 165)
    g.drawLine(x + 18, y - 45, x + 155, y - 175)
    g.drawLine(x - 8, y - 70, x - 75, y - 220)
    g.drawLine(x + 10, y - 75, x + 75, y - 225)

    val crowns = Array(
      (-150, -170, 115, 85), (-70, -225, 125, 95),
      (55, -230, 125, 95), (145, -170, 115, 85),
      (0, -155, 150, 110)
    )
    crowns.zipWithIndex.foreach { entry =>
      val c = entry._1
      val idx = entry._2
      g.setColor(if (idx % 2 == 0) new Color(52, 158, 113, 235) else new Color(73, 190, 133, 235))
      g.fillOval(x + c._1, y + c._2, c._3, c._4)
      g.setColor(new Color(177, 255, 197, 110))
      g.drawArc(x + c._1 + 10, y + c._2 + 8, c._3 - 20, c._4 - 20, 15, 145)
    }

    // Wing-shaped light around the Sanctus canopy.
    g.setColor(new Color(176, 246, 255, 115))
    g.fillPolygon(Array(x - 72, x - 210, x - 155, x - 45),
      Array(y - 170, y - 260, y - 120, y - 105), 4)
    g.fillPolygon(Array(x + 72, x + 210, x + 155, x + 45),
      Array(y - 170, y - 260, y - 120, y - 105), 4)
    g.setColor(new Color(236, 255, 245, 160))
    g.drawLine(x - 72, y - 170, x - 190, y - 245)
    g.drawLine(x + 72, y - 170, x + 190, y - 245)

    // Suspended rings and glowing seed altar.
    g.setColor(new Color(112, 244, 222, 170))
    g.setStroke(new BasicStroke(3.0f))
    g.drawOval(x - 112, y - 125, 224, 52)
    g.drawOval(x - 88, y - 143, 176, 38)
    g.setColor(new Color(255, 224, 130, 220))
    g.fillRoundRect(x - 76, y + 275, 152, 28, 12, 12)
    g.setColor(new Color(190, 255, 225, 220))
    g.fillOval(x - 22, y + 225, 44, 60)
    drawGlow(g, x, y + 250, 72, GOLD, 0.22f)

    for (i <- 0 until 12) {
      val a = frames * 0.018 + i * math.Pi * 2.0 / 12.0
      val px = x + (math.cos(a) * (118 + 10 * pulse)).toInt
      val py = y - 95 + (math.sin(a) * 65).toInt
      g.setColor(if (i % 2 == 0) CYAN else GOLD)
      g.fillOval(px - 4, py - 4, 8, 8)
    }

    g.setFont(new Font("SansSerif", Font.BOLD, 12))
    g.setColor(new Color(215, 255, 232))
    drawCentered(g, "SANCTUS: ETERNAL BLOOM", x, y + 338)
  }

  def drawSwingVines(g: Graphics2D): Unit = {
    swingVines.foreach { vine =>
      val ax = sx(vine.x1)
      val ay = sy(vine.y1)
      val bx = sx(vine.x2)
      val by = sy(vine.y2)
      val controlX = (ax + bx) / 2
      val controlY = (ay + by) / 2 + 32
      var previousX = ax
      var previousY = ay
      var step = 1
      g.setStroke(new BasicStroke(5.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
      g.setColor(hueColor(vine.hue, 0.58f, 0.78f))
      while (step <= 12) {
        val t = step / 12.0
        val inv = 1.0 - t
        val px = (inv * inv * ax + 2.0 * inv * t * controlX + t * t * bx).toInt
        val py = (inv * inv * ay + 2.0 * inv * t * controlY + t * t * by).toInt
        g.drawLine(previousX, previousY, px, py)
        previousX = px
        previousY = py
        step += 1
      }
      drawGlow(g, ax, ay, 20, hueColor(vine.hue), 0.16f)
      drawGlow(g, bx, by, 20, hueColor(vine.hue), 0.16f)
      g.setColor(new Color(205, 255, 225, 220))
      g.fillOval(ax - 5, ay - 5, 10, 10)
      g.fillOval(bx - 5, by - 5, 10, 10)
      g.setFont(new Font("SansSerif", Font.BOLD, 9))
      g.setColor(new Color(220, 255, 243))
      drawCentered(g, "Q - SWING", controlX, controlY + 15)
    }
  }

  def drawCrystal(g: Graphics2D, c: Crystal): Unit = {

    if (c.collected) return

    val x = sx(c.x)
    val y = sy(c.y)

    if (x < -90 || y < -90 || x > SCREEN_W + 90 || y > SCREEN_H + 90) return

    val color = hueColor(c.hue)
    val r = (16 + 3 * math.sin(frames * 0.08 + c.hue)).toInt

    if (glowMode) {
      drawGlow(g, x, y, r * 4, color, 0.13f)
    }

    val px = Array(x, x + r, x, x - r)
    val py = Array(y - r, y, y + r, y)

    g.setColor(color)
    g.fillPolygon(px, py, 4)

    g.setColor(WHITE)
    g.drawPolygon(px, py, 4)

    g.setFont(new Font("SansSerif", Font.BOLD, 10))
    drawCentered(g, c.label, x, y - r - 19)

    g.setColor(CYAN)
    drawCentered(g, "E - COLLECT", x, y + r + 18)
  }

  def drawVillain(g: Graphics2D, v: Villain): Unit = {

    val x = sx(v.x)
    val y = sy(v.y)

    if (x < -70 || y < -70 || x > SCREEN_W + 70 || y > SCREEN_H + 70) return

    drawGlow(g, x, y, 34, hueColor(v.hue), 0.12f)

    g.setColor(new Color(31, 23, 60))
    g.fillOval(x - 24, y - 22, 48, 44)

    g.setColor(hueColor(v.hue, 0.76f, 0.95f))
    g.fillOval(x - 20, y - 20, 40, 37)

    g.setColor(new Color(255, 245, 220))
    g.fillOval(x - 11, y - 5, 7, 9)
    g.fillOval(x + 4, y - 5, 7, 9)

    g.setColor(new Color(30, 24, 48))
    g.fillOval(x - 9, y - 2, 3, 4)
    g.fillOval(x + 6, y - 2, 3, 4)
    g.drawArc(x - 8, y + 3, 16, 9, 190, 160)

    g.setColor(WHITE)
    g.setFont(new Font("SansSerif", Font.BOLD, 9))
    drawCentered(g, "SHADOW", x, y - 30)
  }

  def drawPortal(g: Graphics2D): Unit = {

    val x = sx(portalX)
    val y = sy(portalY)

    val ready = collectedCount == crystals.length
    val c = if (ready) MINT else PURPLE

    val pulse = 1.0 + math.sin(frames * 0.05) * 0.06
    val w = (116 * pulse).toInt
    val h = (148 * pulse).toInt

    if (glowMode) drawGlow(g, x, y, 105, c, 0.12f)

    g.setColor(new Color(8, 14, 35, 235))
    g.fillOval(x - w / 2, y - h / 2, w, h)

    g.setColor(c)
    g.setStroke(new BasicStroke(5.0f))
    g.drawOval(x - w / 2, y - h / 2, w, h)

    g.setColor(new Color(80, 230, 245, 140))
    g.setStroke(new BasicStroke(2.0f))
    g.drawOval(x - w / 2 + 12, y - h / 2 + 13, w - 24, h - 26)

    for (i <- 0 until 7) {
      val a = frames * 0.03 + i * math.Pi * 2.0 / 7.0
      val px = x + (math.cos(a) * 43).toInt
      val py = y + (math.sin(a) * 56).toInt
      g.setColor(if (i % 2 == 0) CYAN else PINK)
      g.fillOval(px - 3, py - 3, 6, 6)
    }

    g.setFont(new Font("SansSerif", Font.BOLD, 11))
    g.setColor(c)
    drawCentered(g, if (ready) "PORTAL READY" else "PORTAL LOCKED", x, y + h / 2 + 20)
  }

  def drawPlayer(g: Graphics2D, xWorld: Double, yWorld: Double, isLocal: Boolean): Unit = {

    val x = sx(xWorld)
    val y = sy(yWorld)

    val bob =
      if (isLocal) math.sin(frames * 0.2) * 2.0
      else 0.0

    if (isLocal && glowMode) drawGlow(g, x, y, 39, CYAN, 0.10f)

    if (activeLevel == 4) {
      val flap = (math.sin(frames * 0.22) * 9.0).toInt
      g.setColor(new Color(150, 238, 255, 175))
      g.fillPolygon(Array(x - 9, x - 42, x - 35, x - 17),
        Array(y - 18, y - 42 - flap, y - 5, y + 4), 4)
      g.fillPolygon(Array(x + 9, x + 42, x + 35, x + 17),
        Array(y - 18, y - 42 - flap, y - 5, y + 4), 4)
      g.setColor(new Color(235, 255, 255, 180))
      g.drawLine(x - 10, y - 17, x - 38, y - 38 - flap)
      g.drawLine(x + 10, y - 17, x + 38, y - 38 - flap)
      if (wingBoostTime > 0.0) drawGlow(g, x, y, 58, GOLD, 0.18f)
    }

    g.setColor(new Color(0, 0, 0, 80))
    g.fillOval(x - 18, y + 14, 36, 10)

    g.setColor(new Color(30, 45, 78))
    g.fillRoundRect(x - 12, y + bob.toInt, 10, 20, 5, 5)
    g.fillRoundRect(x + 3, y - bob.toInt, 10, 20, 5, 5)

    g.setColor(CYAN)
    g.fillRoundRect(x - 13, y + 15, 12, 6, 3, 3)
    g.fillRoundRect(x + 2, y + 15, 12, 6, 3, 3)

    g.setColor(new Color(34, 80, 137))
    g.fillRoundRect(x - 17, y - 25 + bob.toInt, 34, 34, 10, 10)

    g.setColor(new Color(57, 190, 194))
    g.fillRoundRect(x - 13, y - 21 + bob.toInt, 26, 25, 8, 8)

    g.setColor(new Color(239, 207, 177))
    g.fillOval(x - 11, y - 47 + bob.toInt, 22, 23)

    g.setColor(new Color(38, 30, 48))
    g.fillArc(x - 12, y - 53 + bob.toInt, 24, 25, 0, 180)

    g.setColor(new Color(13, 42, 70))
    g.fillRoundRect(x - 9, y - 38 + bob.toInt, 18, 7, 4, 4)

    g.setColor(CYAN)
    g.drawLine(
      x - 5, y - 35 + bob.toInt,
      x + 5, y - 35 + bob.toInt
    )

    if (isLocal) {
      g.setFont(new Font("SansSerif", Font.BOLD, 10))
      g.setColor(WHITE)
      drawCentered(g, currentUser, x, y - 62)
    }
  }

  // FIX 2: Complete animated particle drawing method.
  def drawParticles(g: Graphics2D): Unit = {

    particles.foreach { p =>

      val x = sx(p.x)
      val y = sy(p.y)

      if (
        x >= -12 &&
        y >= -12 &&
        x <= SCREEN_W + 12 &&
        y <= SCREEN_H + 12
      ) {

        val alpha = clamp(p.life / 4.0, 0.0, 0.82).toFloat
        val oldComposite = g.getComposite

        try {
          g.setComposite(
            AlphaComposite.SrcOver.derive(alpha)
          )

          g.setColor(
            hueColor(p.hue, 0.66f, 1.0f)
          )

          val size = math.max(2, p.size.toInt)

          g.fillOval(
            x - size / 2,
            y - size / 2,
            size,
            size
          )

          g.setColor(new Color(255, 255, 255, 150))
          g.fillOval(x - 1, y - 1, math.max(1, size / 3), math.max(1, size / 3))

        } finally {
          g.setComposite(oldComposite)
        }
      }
    }
  }

  def drawNetworkPlayers(g: Graphics2D): Unit = {

    def drawEntries(entries: Seq[(String, String)]): Unit = {

      entries.foreach { pair =>

        val id = pair._1

        if (
          !id.equals("HOST-" + safeName(currentUser)) &&
          !id.startsWith(safeName(currentUser) + "-")
        ) {

          val xy = pair._2.split(",", 2)

          if (xy.length == 2) {
            try {
              drawPlayer(
                g,
                xy(0).toDouble,
                xy(1).toDouble,
                false
              )

              g.setFont(new Font("SansSerif", Font.BOLD, 10))
              g.setColor(GOLD)

              drawCentered(
                g,
                id.take(18),
                sx(xy(0).toDouble),
                sy(xy(1).toDouble) - 61
              )

            } catch {
              case _: NumberFormatException => ()
            }
          }
        }
      }
    }

    multiplayerMode match {
      case "HOST" => lanServer.foreach(s => drawEntries(s.snapshot()))
      case "JOIN" => lanClient.foreach(c => drawEntries(c.snapshot()))
      case _ => ()
    }
  }

  def drawMap(g: Graphics2D, w: Int): Unit = {

    val x = w - 188
    val y = 115
    val mw = 166
    val mh = 112

    g.setColor(new Color(5, 13, 29, 220))
    g.fillRoundRect(x, y, mw, mh, 13, 13)

    g.setColor(new Color(70, 225, 220, 170))
    g.drawRoundRect(x, y, mw, mh, 13, 13)

    def mx(v: Double): Int =
      x + 8 + (v / WORLD_W * (mw - 16)).toInt

    def my(v: Double): Int =
      y + 8 + (v / WORLD_H * (mh - 16)).toInt

    crystals.filter(!_.collected).foreach { c =>
      g.setColor(hueColor(c.hue))
      g.fillOval(mx(c.x) - 3, my(c.y) - 3, 6, 6)
    }

    g.setColor(MINT)
    g.fillOval(mx(portalX) - 4, my(portalY) - 4, 8, 8)

    g.setColor(WHITE)
    g.fillOval(mx(playerX) - 3, my(playerY) - 3, 6, 6)

    g.setFont(new Font("SansSerif", Font.BOLD, 9))
    g.drawString("LEVEL MAP", x + 9, y + 13)
  }

  def drawHUD(g: Graphics2D, w: Int, h: Int): Unit = {

    g.setColor(new Color(5, 15, 32, 225))
    g.fillRoundRect(15, 14, 535, 132, 17, 17)

    g.setColor(new Color(75, 230, 219, 170))
    g.drawRoundRect(15, 14, 535, 132, 17, 17)

    g.setFont(new Font("SansSerif", Font.BOLD, 18))
    g.setColor(CYAN)
    g.drawString("BIXUU  /  SPRING SEASON", 29, 40)

    g.setFont(new Font("SansSerif", Font.BOLD, 11))
    g.setColor(WHITE)
    g.drawString(
      "LEVEL " + activeLevel + " - " + levels(activeLevel).name,
      29,
      61
    )

    g.setFont(new Font("SansSerif", Font.PLAIN, 10))
    g.setColor(GOLD)
    g.drawString("EVENT: " + eventNameForLevel(activeLevel).take(43), 29, 78)

    g.setFont(new Font("SansSerif", Font.PLAIN, 10))
    g.setColor(new Color(190, 225, 235))
    g.drawString("DIFFICULTY: " + activeDifficulty + "   |   MODE: " + activeGameMode, 29, 96)

    g.setFont(new Font("SansSerif", Font.BOLD, 11))
    g.setColor(WHITE)
    g.drawString(
      "CRYSTALS  " + collectedCount + " / " + crystals.length,
      29,
      121
    )

    g.setColor(new Color(34, 55, 72))
    g.fillRoundRect(250, 111, 245, 9, 8, 8)

    g.setColor(MINT)
    g.fillRoundRect(
      250, 111,
      (245.0 * collectedCount / crystals.length).toInt,
      9, 8, 8
    )

    if (activeLevel == 4) {
      g.setFont(new Font("SansSerif", Font.BOLD, 10))
      g.setColor(new Color(185, 255, 229))
      g.drawString("WING CHARGES: " + wingCharges + "   |   VINE SWING: " + (if (swingCooldown <= 0.0) "READY" else "COOLDOWN"), 29, 139)
    }

    val mm = remainingSeconds.toInt / 60
    val ss = remainingSeconds.toInt % 60

    g.setColor(new Color(5, 15, 32, 225))
    g.fillRoundRect(w - 185, 17, 165, 70, 14, 14)

    g.setFont(new Font("Monospaced", Font.BOLD, 22))
    g.setColor(if (remainingSeconds < 40) PINK else GOLD)

    g.drawString(
      "%02d:%02d".format(mm, ss),
      w - 164,
      49
    )

    g.setFont(new Font("SansSerif", Font.PLAIN, 10))
    g.setColor(WHITE)
    g.drawString(if (activeGameMode == "SURVIVAL") "SURVIVAL TIME" else "TIME LEFT", w - 162, 70)

    g.setColor(new Color(5, 15, 32, 225))
    g.fillRoundRect(15, h - 49, w - 30, 35, 12, 12)

    g.setFont(new Font("SansSerif", Font.BOLD, 11))
    g.setColor(MINT)
    g.drawString("WASD / ARROWS: MOVE", 28, h - 27)

    g.setColor(CYAN)
    g.drawString("E: COLLECT", 220, h - 27)

    g.setColor(PINK)
    g.drawString("SPACE: DASH", 330, h - 27)

    g.setColor(GOLD)
    g.drawString("F: GLOW", 455, h - 27)

    if (activeLevel == 4) {
      g.setColor(new Color(175, 245, 255))
      g.drawString("Q: SWING", 525, h - 27)
      g.setColor(new Color(255, 221, 145))
      g.drawString("X: WINGS", 615, h - 27)
    }

    g.setColor(WHITE)
    g.drawString("M: MAP   ESC: PAUSE   R: RETRY / MENU", if (activeLevel == 4) 720 else 555, h - 27)

    g.setFont(new Font("SansSerif", Font.BOLD, 13))
    g.setColor(PINK)
    g.drawString("HEARTS: " + ("? " * health), 570, 42)

    g.setColor(MUTED)
    g.setFont(new Font("SansSerif", Font.PLAIN, 10))

    g.drawString(
      if (multiplayerMode == "SOLO") "SOLO"
      else "LAN MULTIPLAYER - " + multiplayerMode,
      570,
      61
    )

    if (showMap) drawMap(g, w)

    if (toastTime > 0.0) {

      val tw = math.min(
        w - 50,
        math.max(340, g.getFontMetrics.stringWidth(toast) + 36)
      )

      val tx = (w - tw) / 2

      g.setColor(new Color(5, 16, 34, 235))
      g.fillRoundRect(tx, h - 96, tw, 32, 14, 14)

      g.setColor(CYAN)
      g.drawRoundRect(tx, h - 96, tw, 32, 14, 14)

      g.setFont(new Font("SansSerif", Font.BOLD, 11))
      g.setColor(WHITE)

      drawCentered(g, toast, w / 2, h - 76)
    }
  }

  def drawOverlay(g: Graphics2D, w: Int, h: Int): Unit = {

    g.setColor(new Color(3, 8, 22, 210))
    g.fillRect(0, 0, w, h)

    val title =
      if (won) "LEVEL COMPLETE!"
      else if (lost) "ADVENTURE OVER"
      else "PAUSED"

    val sub =
      if (won) {
        if (activeLevel == 4) "You awakened the Sanctus Heart and restored the Eternal Bloom!"
        else "You restored the spring and opened the portal."
      } else if (lost) {
        "Try again and avoid the garden shadows."
      } else {
        "Your adventure is paused."
      }

    g.setFont(new Font("SansSerif", Font.BOLD, 38))
    g.setColor(if (won) MINT else if (lost) PINK else CYAN)

    drawCentered(g, title, w / 2, h / 2 - 45)

    g.setFont(new Font("SansSerif", Font.PLAIN, 16))
    g.setColor(WHITE)
    drawCentered(g, sub, w / 2, h / 2 - 8)

    g.setFont(new Font("SansSerif", Font.BOLD, 15))
    g.setColor(GOLD)

    drawCentered(
      g,
      if (won) "ENTER: NEXT LEVEL     R: LEVEL SELECT"
      else "R: RETRY THIS LEVEL     ESC: RESUME",
      w / 2,
      h / 2 + 40
    )
  }

  // ============================================================
  // GAME PANEL
  // ============================================================

  final class GamePanel extends JPanel {

    setPreferredSize(new Dimension(SCREEN_W, SCREEN_H))
    setBackground(BG_TOP)
    setFocusable(true)
    setDoubleBuffered(true)

    addKeyListener(new KeyAdapter {

      override def keyPressed(e: KeyEvent): Unit = {

        val k = e.getKeyCode
        val fresh = !keys.contains(k)

        keys += k

        if (fresh) {

          if (!started && (k == KeyEvent.VK_ENTER || k == KeyEvent.VK_SPACE)) {
            started = true
          }

          else if (k == KeyEvent.VK_E) {
            collectOrUse()
          }

          else if (k == KeyEvent.VK_Q) {
            useSwingVine()
          }

          else if (k == KeyEvent.VK_X) {
            activateWings()
          }

          else if (k == KeyEvent.VK_SPACE && started && !won && !lost) {
            if (dashCooldown <= 0.0) {
              dashTime = 0.22
              dashCooldown = 1.0
            }
          }

          else if (k == KeyEvent.VK_F) {
            glowMode = !glowMode
          }

          else if (k == KeyEvent.VK_M) {
            showMap = !showMap
          }

          else if (k == KeyEvent.VK_ESCAPE && !won && !lost) {
            paused = !paused
          }

          else if (k == KeyEvent.VK_R) {

            if (won) {
              returnToLevels()
            }

            else if (lost) {
              if (multiplayerMode == "SOLO") restartCurrentLevel()
              else returnToLevels()
            }

            else if (paused) {
              paused = false
            }

            else {
              returnToLevels()
            }
          }

          else if (k == KeyEvent.VK_ENTER && won) {

            if (multiplayerMode != "SOLO") {
              JOptionPane.showMessageDialog(
                frame,
                "Return to Level Select before starting another multiplayer adventure."
              )
              returnToLevels()
            }

            else if (activeLevel < 4) {
              startGame(activeLevel + 1, "SOLO")
            }

            else {
              returnToLevels()
            }
          }
        }
      }

      override def keyReleased(e: KeyEvent): Unit = {
        keys -= e.getKeyCode
      }
    })

    addMouseListener(new MouseAdapter {
      override def mouseClicked(e: MouseEvent): Unit = {
        requestFocusInWindow()
      }
    })

    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
        )

        val w = getWidth
        val h = getHeight

        cameraX = clamp(
          playerX - w / 2.0,
          0.0,
          WORLD_W - w
        )

        cameraY = clamp(
          playerY - h / 2.0,
          0.0,
          WORLD_H - h
        )

        drawBackground(g, w, h)
        drawGround(g, w, h)
        if (activeLevel == 4) drawSwingVines(g)
        if (activeLevel == 4) drawSanctusSanctuary(g)
        else drawWaterfall(g)

        trees.foreach(t => drawTree(g, t))
        flowers.foreach(f => drawFlower(g, f))
        crystals.foreach(c => drawCrystal(g, c))
        villains.foreach(v => drawVillain(g, v))

        drawPortal(g)
        drawParticles(g)
        drawNetworkPlayers(g)
        drawPlayer(g, playerX, playerY, true)

        drawHUD(g, w, h)

        if (paused || won || lost) {
          drawOverlay(g, w, h)
        }

      } finally {
        g.dispose()
      }
    }
  }

  // ============================================================
  // LOGIN SCREEN
  // ============================================================

  def buildLoginScreen(): JPanel = {

    val p = verticalPanel()

    p.add(Box.createVerticalGlue())

    addCentered(p, heading("BIXUU", 22, MINT), 4)
    addCentered(p, heading("SPRING SEASON", 39, CYAN), 8)
    addCentered(p, heading("EVENT ADVENTURE WORLD", 18, PINK), 2)

    addCentered(
      p,
      bodyLabel("Login with a player name to enter the garden."),
      18
    )

    val form = new JPanel(new GridBagLayout())
    form.setOpaque(false)

    val c = new GridBagConstraints()
    c.insets = new Insets(7, 7, 7, 7)
    c.fill = GridBagConstraints.HORIZONTAL

    c.gridx = 0
    c.gridy = 0

    val nameLabel = new JLabel("Player name")
    nameLabel.setForeground(WHITE)
    form.add(nameLabel, c)

    c.gridx = 1

    nameField = new JTextField("Player", 18)
    form.add(nameField, c)

    c.gridx = 0
    c.gridy = 1

    val pinLabel = new JLabel("Admin password (admin only)")
    pinLabel.setForeground(WHITE)
    form.add(pinLabel, c)

    c.gridx = 1

    pinField = new JPasswordField(18)
    form.add(pinField, c)

    p.add(Box.createVerticalStrut(12))
    p.add(form)

    val login = styledButton("LOGIN / CREATE PROFILE")

    addCentered(p, login, 12)

    addCentered(
      p,
      bodyLabel("Admin account: BIXUU@GC  |  Password is private."),
      7
    )

    addCentered(
      p,
      bodyLabel("Enter a player name to play. Admin access requires the private password."),
      4
    )

    p.add(Box.createVerticalGlue())

    addAction(login) {

      val typed = safeName(nameField.getText)
      val pin = new String(pinField.getPassword)
      pinField.setText("")

      if (typed.equalsIgnoreCase(ADMIN_NAME) && pin == ADMIN_PIN) {
        currentUser = ADMIN_NAME
        isAdmin = true
      }

      else {
        currentUser = typed
        isAdmin = false

        if (typed.equalsIgnoreCase(ADMIN_NAME)) {
          JOptionPane.showMessageDialog(
            frame,
            "Admin PIN is not correct. You will enter as a regular player."
          )
        }
      }

      updateMenuWelcome()
      adminButton.setVisible(isAdmin)
      showCard("menu")
    }

    p
  }

  // ============================================================
  // MAIN MENU
  // ============================================================

  def buildMenuScreen(): JPanel = {

    val p = verticalPanel()

    p.add(Box.createVerticalGlue())

    p.add(heading("BIXUU SPRING SEASON", 32, CYAN))
    p.add(Box.createVerticalStrut(5))

    welcomeLabel = bodyLabel("Welcome, Player")
    p.add(welcomeLabel)

    p.add(Box.createVerticalStrut(24))

    val levelsBtn = styledButton("LEVELS & ADVENTURES")
    val multiBtn = styledButton("MULTIPLAYER - HOTSPOT / WI-FI")

    adminButton = styledButton("BIXUU@GC ADMIN PANEL")
    val logoutBtn = styledButton("LOG OUT")

    addCentered(p, levelsBtn, 8)
    addCentered(p, multiBtn, 8)
    addCentered(p, adminButton, 8)
    addCentered(p, logoutBtn, 8)

    adminButton.setVisible(false)

    p.add(Box.createVerticalGlue())

    addAction(levelsBtn) {
      refreshLevelButtons()
      showCard("levels")
    }

    addAction(multiBtn) {
      showCard("multiplayer")
    }

    addAction(adminButton) {
      refreshEventList()
      showCard("admin")
    }

    addAction(logoutBtn) {

      closeNetwork()

      if (gameTimer != null) gameTimer.stop()

      currentUser = "Player"
      isAdmin = false

      nameField.setText("Player")
      pinField.setText("")
      adminButton.setVisible(false)

      showCard("login")
    }

    p
  }

  def updateMenuWelcome(): Unit = {
    if (welcomeLabel != null) {
      welcomeLabel.setText(
        "Welcome, " + currentUser +
        (if (isAdmin) " - ADMIN" else " - PLAYER")
      )
    }
  }

  // ============================================================
  // LEVEL SELECT
  // ============================================================

  def buildLevelsScreen(): JPanel = {

    val p = verticalPanel()

    addCentered(p, heading("CHOOSE YOUR ADVENTURE", 30, CYAN), 3)
    addCentered(p, bodyLabel("Complete each level to unlock the next adventure."), 3)

    addCentered(p, heading("LEVEL 1 - WHISPERING WOODS", 17, MINT), 8)
    addCentered(p, bodyLabel("Collect 5 nature seeds.  |  1 Shadow"), 1)

    addCentered(p, heading("LEVEL 2 - AURORA FALLS", 17, PINK), 7)
    addCentered(p, bodyLabel("Collect 6 cosmic crystals.  |  2 Shadows"), 1)

    addCentered(p, heading("LEVEL 3 - CELESTIAL WATERFALL TEMPLE", 17, GOLD), 7)
    addCentered(p, bodyLabel("Collect 7 temple gems.  |  3 Shadows"), 1)

    level04Heading = heading("LEVEL 4 - " + event03Title.toUpperCase, 18, MINT)
    addCentered(p, level04Heading, 7)
    level04Details = bodyLabel("EVENT 03: " + event03Title + "  |  WINGS & SWINGS  |  " + event03Difficulty + " / " + event03Mode)
    addCentered(p, level04Details, 1)

    levelButtons.clear()
    val b1 = styledButton("PLAY LEVEL 1")
    val b2 = styledButton("PLAY LEVEL 2 - LOCKED")
    val b3 = styledButton("PLAY LEVEL 3 - LOCKED")
    val b4 = styledButton("PLAY EVENT 03 - LOCKED")

    levelButtons += b1
    levelButtons += b2
    levelButtons += b3
    levelButtons += b4

    addCentered(p, b1, 7)
    addCentered(p, b2, 5)
    addCentered(p, b3, 5)
    addCentered(p, b4, 5)

    val back = styledButton("BACK TO MAIN MENU")
    addCentered(p, back, 8)
    p.add(Box.createVerticalGlue())

    addAction(b1) { startGame(1, "SOLO") }

    addAction(b2) {
      if (completedLevels.contains(1)) startGame(2, "SOLO")
      else JOptionPane.showMessageDialog(frame, "Complete Level 1 first to unlock Aurora Falls.")
    }

    addAction(b3) {
      if (completedLevels.contains(2)) startGame(3, "SOLO")
      else JOptionPane.showMessageDialog(frame, "Complete Level 2 first to unlock Celestial Waterfall Temple.")
    }

    addAction(b4) {
      if (completedLevels.contains(3)) startGame(4, "SOLO")
      else JOptionPane.showMessageDialog(frame, "Complete Level 3 first to unlock Event 03: Sanctus Eternal Bloom.")
    }

    addAction(back) { showCard("menu") }
    refreshLevelButtons()
    p
  }

  def refreshLevelButtons(): Unit = {
    if (level04Heading != null) level04Heading.setText("LEVEL 4 - " + event03Title.toUpperCase.take(48))
    if (level04Details != null) level04Details.setText("EVENT 03: " + event03Title + "  |  WINGS & SWINGS  |  " + event03Difficulty + " / " + event03Mode)

    if (levelButtons.length == 4) {
      levelButtons(0).setEnabled(true)
      levelButtons(0).setText(
        if (completedLevels.contains(1)) "REPLAY LEVEL 1 - COMPLETED" else "PLAY LEVEL 1 - WHISPERING WOODS"
      )

      levelButtons(1).setEnabled(completedLevels.contains(1))
      levelButtons(1).setText(
        if (completedLevels.contains(2)) "REPLAY LEVEL 2 - COMPLETED"
        else if (completedLevels.contains(1)) "PLAY LEVEL 2 - AURORA FALLS"
        else "PLAY LEVEL 2 - LOCKED"
      )

      levelButtons(2).setEnabled(completedLevels.contains(2))
      levelButtons(2).setText(
        if (completedLevels.contains(3)) "REPLAY LEVEL 3 - COMPLETED"
        else if (completedLevels.contains(2)) "PLAY LEVEL 3 - CELESTIAL TEMPLE"
        else "PLAY LEVEL 3 - LOCKED"
      )

      levelButtons(3).setEnabled(completedLevels.contains(3))
      levelButtons(3).setText(
        if (completedLevels.contains(4)) "REPLAY EVENT 03 - COMPLETED"
        else if (completedLevels.contains(3)) "PLAY EVENT 03 - " + event03Title.take(34)
        else "PLAY EVENT 03 - LOCKED"
      )
    }
  }

  // ============================================================
  // MULTIPLAYER LOBBY
  // ============================================================

  def buildMultiplayerScreen(): JPanel = {

    val p = verticalPanel()

    addCentered(p, heading("MULTIPLAYER LOBBY", 31, CYAN), 4)

    addCentered(
      p,
      bodyLabel("Play using the same Wi-Fi or mobile hotspot."),
      5
    )

    addCentered(
      p,
      bodyLabel("One device hosts a room. Other players join using the host IP."),
      3
    )

    hostIpLabel = bodyLabel(
      "Host IP: start a room to see the local address"
    )

    addCentered(p, hostIpLabel, 15)

    val hostPort = new JTextField(String.valueOf(DEFAULT_PORT), 8)
    val joinIp = new JTextField("192.168.1.2", 15)
    val joinPort = new JTextField(String.valueOf(DEFAULT_PORT), 8)

    val roomLevel = new JComboBox[String](Array(
      "Level 1 - Whispering Woods",
      "Level 2 - Aurora Falls",
      "Level 3 - Celestial Waterfall Temple",
      "Level 4 - Sanctus Eternal Bloom (Event 03)"
    ))

    val levelRow = new JPanel()
    levelRow.setOpaque(false)

    levelRow.add(labelFor("Adventure level:"))
    levelRow.add(roomLevel)

    p.add(levelRow)

    val hostRow = new JPanel()
    hostRow.setOpaque(false)

    hostRow.add(labelFor("Host port:"))
    hostRow.add(hostPort)

    val hostBtn = styledButton("HOST MULTIPLAYER ROOM")

    p.add(hostRow)
    addCentered(p, hostBtn, 6)

    addCentered(
      p,
      bodyLabel("--- OR JOIN A FRIEND'S ROOM ---"),
      15
    )

    val joinRow = new JPanel()
    joinRow.setOpaque(false)

    joinRow.add(labelFor("Host IP:"))
    joinRow.add(joinIp)
    joinRow.add(labelFor("Port:"))
    joinRow.add(joinPort)

    p.add(joinRow)

    val joinBtn = styledButton("JOIN MULTIPLAYER ROOM")

    addCentered(p, joinBtn, 9)

    multiplayerStatus = bodyLabel("Status: not connected")

    addCentered(p, multiplayerStatus, 8)

    val back = styledButton("BACK TO MAIN MENU")

    addCentered(p, back, 13)

    p.add(Box.createVerticalGlue())

    addAction(hostBtn) {

      try {

        val port = hostPort.getText.trim.toInt

        require(
          port >= 1024 && port <= 65535,
          "Port must be between 1024 and 65535."
        )

        closeNetwork()

        val server = new LanServer(port, currentUser)
        server.start()

        lanServer = Some(server)

        val ip =
          try InetAddress.getLocalHost.getHostAddress
          catch {
            case _: Throwable => "Check your Wi-Fi IPv4 address"
          }

        hostIpLabel.setText(
          "HOST IP: " + ip + "    PORT: " + port
        )

        multiplayerStatus.setText("Status: room hosted.")

        JOptionPane.showMessageDialog(
          frame,
          "Your room is hosted.\n\nHost IP: " + ip +
          "\nPort: " + port +
          "\n\nOther devices must connect to the same Wi-Fi or mobile hotspot.",
          "Multiplayer room",
          JOptionPane.INFORMATION_MESSAGE
        )

        startGame(roomLevel.getSelectedIndex + 1, "HOST")

      } catch {
        case ex: Throwable =>
          JOptionPane.showMessageDialog(
            frame,
            "Could not host room: " + ex.getMessage +
            "\nTry another port or allow Java through the firewall.",
            "Host error",
            JOptionPane.ERROR_MESSAGE
          )
      }
    }

    addAction(joinBtn) {

      try {

        val ip = joinIp.getText.trim
        val port = joinPort.getText.trim.toInt

        require(ip.nonEmpty, "Enter the host IP.")
        require(port >= 1024 && port <= 65535, "Invalid port.")

        closeNetwork()

        val client = new LanClient(ip, port, currentUser)
        lanClient = Some(client)

        multiplayerStatus.setText(
          "Status: connected to " + ip + ":" + port
        )

        startGame(roomLevel.getSelectedIndex + 1, "JOIN")

      } catch {
        case ex: Throwable =>
          JOptionPane.showMessageDialog(
            frame,
            "Could not join room: " + ex.getMessage +
            "\nCheck the host IP, port, hotspot and firewall.",
            "Join error",
            JOptionPane.ERROR_MESSAGE
          )
      }
    }

    addAction(back) {
      showCard("menu")
    }

    p
  }

  def labelFor(text: String): JLabel = {
    val l = new JLabel(text)
    l.setForeground(WHITE)
    l.setFont(new Font("SansSerif", Font.BOLD, 13))
    l
  }

  // ============================================================
  // ADMIN PANEL
  // ============================================================

  def refreshEventList(): Unit = {
    if (eventListArea != null) {
      val all = ArrayBuffer[String](
        "SPRING SEASON - EVENT 01: GLITCH IN THE GARDEN",
        "SPRING SEASON - EVENT 02: " + activeEventTitle,
        "SPRING SEASON - EVENT 03: " + event03Title,
        "EVENT 03 THEME: WINGS & SWINGS",
        "EVENT 03 SETTINGS: " + event03Difficulty + " / " + event03Mode
      )
      extraEvents.foreach(e => all += ("CUSTOM EVENT: " + e))
      extraEvents03.foreach(e => all += ("EVENT 03 MODE CARD: " + e))
      eventListArea.setText(all.mkString("\n"))
    }

    if (eventTitleField != null) eventTitleField.setText(activeEventTitle)
    if (event03TitleField != null) event03TitleField.setText(event03Title)
    if (adminDifficultyBox != null) adminDifficultyBox.setSelectedItem(event03Difficulty)
    if (adminModeBox != null) adminModeBox.setSelectedItem(event03Mode)
  }

  def buildAdminScreen(): JPanel = {

    val content = verticalPanel()
    addCentered(content, heading("BIXUU@GC ADMIN CONTROL ROOM", 27, GOLD), 3)
    addCentered(content, bodyLabel("Manage Spring Season events, Event 03 difficulty, WINGS & SWINGS traversal and built-in game modes."), 4)

    val event02Row = new JPanel()
    event02Row.setOpaque(false)
    event02Row.add(labelFor("Event 02 title:"))
    eventTitleField = new JTextField(activeEventTitle, 27)
    event02Row.add(eventTitleField)
    content.add(event02Row)

    val event03Row = new JPanel()
    event03Row.setOpaque(false)
    event03Row.add(labelFor("Event 03 title:"))
    event03TitleField = new JTextField(event03Title, 27)
    event03Row.add(event03TitleField)
    content.add(event03Row)

    val settingsRow = new JPanel()
    settingsRow.setOpaque(false)
    settingsRow.add(labelFor("Difficulty:"))
    adminDifficultyBox = new JComboBox[String](Array("Easy", "Medium", "Hard", "Expert", "Celestial"))
    adminDifficultyBox.setSelectedItem(event03Difficulty)
    settingsRow.add(adminDifficultyBox)
    settingsRow.add(labelFor("Game mode:"))
    adminModeBox = new JComboBox[String](Array("CLASSIC", "TIME ATTACK", "CRYSTAL RUSH", "SURVIVAL"))
    adminModeBox.setSelectedItem(event03Mode)
    settingsRow.add(adminModeBox)
    content.add(settingsRow)

    val save = styledButton("SAVE EVENT 02 + EVENT 03 SETTINGS")
    addCentered(content, save, 8)

    val newEventRow = new JPanel()
    newEventRow.setOpaque(false)
    val newEventField = new JTextField(25)
    newEventRow.add(labelFor("New custom event card:"))
    newEventRow.add(newEventField)
    content.add(newEventRow)

    val addEvent = styledButton("ADD CUSTOM EVENT CARD")
    addCentered(content, addEvent, 6)

    val newModeRow = new JPanel()
    newModeRow.setOpaque(false)
    val newModeField = new JTextField(25)
    newModeRow.add(labelFor("Event 03 mode card:"))
    newModeRow.add(newModeField)
    content.add(newModeRow)

    val addMode = styledButton("ADD EVENT 03 MODE CARD")
    addCentered(content, addMode, 6)

    eventListArea = new JTextArea(8, 54)
    eventListArea.setEditable(false)
    eventListArea.setLineWrap(true)
    eventListArea.setWrapStyleWord(true)
    eventListArea.setFont(new Font("Monospaced", Font.PLAIN, 13))
    eventListArea.setBackground(new Color(5, 15, 32))
    eventListArea.setForeground(MINT)
    val scroller = new JScrollPane(eventListArea)
    scroller.setAlignmentX(Component.CENTER_ALIGNMENT)
    content.add(Box.createVerticalStrut(8))
    content.add(scroller)

    addCentered(content, bodyLabel("Built-in modes are playable. Event 03 includes Wing Shards, Wings of Dawn and Q-key Swing Vines."), 7)
    addCentered(content, bodyLabel("Admin settings and custom cards reset when the application closes. Built-in controls: Q swings, X uses a collected wing charge."), 2)

    val back = styledButton("BACK TO MAIN MENU")
    addCentered(content, back, 8)
    content.add(Box.createVerticalGlue())

    addAction(save) {
      val title02 = Option(eventTitleField.getText).getOrElse("").trim
      val title03 = Option(event03TitleField.getText).getOrElse("").trim
      if (title02.nonEmpty && title03.nonEmpty) {
        activeEventTitle = title02.take(55)
        event03Title = title03.take(55)
        event03Difficulty = String.valueOf(adminDifficultyBox.getSelectedItem)
        event03Mode = String.valueOf(adminModeBox.getSelectedItem)
        refreshEventList()
        refreshLevelButtons()
        JOptionPane.showMessageDialog(frame, "Event 02 and Event 03 settings saved for this session.")
      } else {
        JOptionPane.showMessageDialog(frame, "Event titles cannot be empty.", "Missing title", JOptionPane.WARNING_MESSAGE)
      }
    }

    addAction(addEvent) {
      val t = Option(newEventField.getText).getOrElse("").trim
      if (t.nonEmpty && !extraEvents.contains(t)) {
        extraEvents += t.take(55)
        newEventField.setText("")
        refreshEventList()
      }
    }

    addAction(addMode) {
      val t = Option(newModeField.getText).getOrElse("").trim
      if (t.nonEmpty && !extraEvents03.contains(t)) {
        extraEvents03 += t.take(45)
        newModeField.setText("")
        refreshEventList()
      }
    }

    addAction(back) { showCard("menu") }
    refreshEventList()

    val scroll = new JScrollPane(content)
    scroll.setBorder(BorderFactory.createEmptyBorder())
    scroll.getViewport.setBackground(new Color(9, 20, 42))
    val outer = new JPanel(new BorderLayout())
    outer.setBackground(new Color(9, 20, 42))
    outer.add(scroll, BorderLayout.CENTER)
    outer
  }

  // ============================================================
  // UI INITIALIZATION
  // ============================================================

  def buildUI(): Unit = {

    cards = new CardLayout()
    root = new JPanel(cards)

    root.add(buildLoginScreen(), "login")
    root.add(buildMenuScreen(), "menu")
    root.add(buildLevelsScreen(), "levels")
    root.add(buildMultiplayerScreen(), "multiplayer")
    root.add(buildAdminScreen(), "admin")

    frame = new JFrame(APP_TITLE)

    frame.setContentPane(root)

    frame.setDefaultCloseOperation(
      WindowConstants.DO_NOTHING_ON_CLOSE
    )

    frame.addWindowListener(
      new java.awt.event.WindowAdapter {

        override def windowClosing(
          e: java.awt.event.WindowEvent
        ): Unit = {

          if (gameTimer != null) {
            gameTimer.stop()
          }

          closeNetwork()
          frame.dispose()
        }
      }
    )

    frame.setMinimumSize(new Dimension(960, 700))
    frame.setSize(1120, 800)
    frame.setLocationRelativeTo(null)
    frame.setVisible(true)

    showCard("login")
  }

  def main(args: Array[String]): Unit = {

    SwingUtilities.invokeLater(
      new Runnable {
        override def run(): Unit = buildUI()
      }
    )
  }
}


// ===== INTEGRATED GAME MODULE: Sanctus_Fourth_Event_3D_Advanced.scala =====

import java.awt.{AlphaComposite, BasicStroke, BorderLayout, Color, Component, Dimension, Font, GradientPaint, Graphics, Graphics2D, RenderingHints}
import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, WindowAdapter, WindowEvent}
import java.io.{BufferedReader, InputStreamReader, PrintWriter}
import java.net.{InetAddress, InetSocketAddress, ServerSocket, Socket}
import java.util.concurrent.{ConcurrentHashMap, ConcurrentLinkedQueue}
import java.util.concurrent.atomic.AtomicInteger
import java.awt.geom.{Ellipse2D, Path2D}
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

/**
 * SPRING SEASON - FOURTH EVENT
 * SANCTUS EVENT
 * Stand-alone Scala + Java Swing game. No external assets or libraries required.
 * Admin account: BIXUU@GC. The existing PIN is checked but never displayed in the UI.
 */
object SanctusFourthEvent {

  // ---------------------------------------------------------------------------
  // APPLICATION SETTINGS
  // ---------------------------------------------------------------------------
  private val APP_TITLE = "SANCTUS EVENT | SPRING SEASON 04"
  private val ADMIN_NAME = "BIXUU@GC"
  // Retains the PIN supplied in the earlier project; it is intentionally not shown in the UI.
  private val ADMIN_PIN = "BIXUU-2026"

  private val WORLD_W = 1900.0
  private val WORLD_H = 1320.0
  private val PORTAL_X = 1710.0
  private val PORTAL_Y = 175.0
  private val SPAWN_X = 135.0
  private val SPAWN_Y = 1130.0

  private val C_BG = new Color(7, 12, 29)
  private val C_PANEL = new Color(13, 25, 48)
  private val C_PANEL_2 = new Color(20, 39, 65)
  private val C_LINE = new Color(74, 117, 150)
  private val C_WHITE = new Color(241, 248, 255)
  private val C_SOFT = new Color(176, 198, 220)
  private val C_MINT = new Color(116, 245, 205)
  private val C_GOLD = new Color(255, 217, 133)
  private val C_PINK = new Color(255, 145, 205)
  private val C_BLUE = new Color(108, 194, 255)
  private val C_VIOLET = new Color(183, 143, 255)

  private val DIFFICULTIES = Array("EASY", "MEDIUM", "HARD", "EXTREME", "NIGHTMARE")
  private val GAME_MODES = Array("ADVENTURE", "TIME ATTACK", "GEM HUNT", "FREE ROAM")
  private val NETWORK_PORT = 5050
  private final case class GemInfo(id: Int, name: String, powerName: String, tint: Color, description: String)
  private val POWER_GEMS: Vector[GemInfo] = Vector(
    GemInfo(0, "RUBY CORE", "Flame Arc", new Color(255, 76, 91), "A focused blast that damages nearby enemies."),
    GemInfo(1, "SAPPHIRE CORE", "Frost Lock", new Color(75, 190, 255), "Freezes nearby enemies for a short time."),
    GemInfo(2, "EMERALD CORE", "Life Bloom", new Color(81, 226, 142), "Restores health and one charge."),
    GemInfo(3, "AMETHYST CORE", "Nova Burst", new Color(190, 112, 255), "Releases a powerful ring of energy."),
    GemInfo(4, "TOPAZ CORE", "Time Bend", new Color(255, 205, 76), "Slows every enemy for a few seconds.")
  )
  // Use Vector values before indexing; direct Array(...)(index) can be parsed as
  // a second argument list for Array.apply on some Scala/Kojo compiler versions.
  private val RELIC_NAMES: Vector[String] = Vector(
    "LUMEN SIGIL", "ORBIT GLASS", "DAWN SEAL",
    "AETHER MARK", "SOLAR TOKEN", "VEIL KEY"
  )
  private val DIFFICULTY_TIME: Vector[Int] = Vector(100, 35, -20, -65, -105)

  private final case class RealmInfo(name: String, caption: String, relics: Int, sentries: Int, hue: Int, time: Int)
  private val realms = Array(
    RealmInfo("MOSSWIND ATRIUM", "A quiet maze of living stone and lantern vines", 5, 1, 145, 360),
    RealmInfo("OPAL TIDEWAY", "A glass coast above a slow-moving sky sea", 6, 2, 190, 345),
    RealmInfo("SOLARIS CANOPY", "A high garden suspended around a sunlit engine", 7, 3, 42, 330),
    RealmInfo("VIOLET SKYFORGE", "A floating foundry where the air carries light", 8, 3, 270, 315),
    RealmInfo("SANCTUM OF FIRST LIGHT", "The final chamber beyond the luminous veil", 9, 4, 315, 300)
  )

  // ---------------------------------------------------------------------------
  // GAME OBJECTS
  // ---------------------------------------------------------------------------
  private final class Relic(val x: Double, val y: Double, val tint: Color, val title: String) {
    var collected = false
  }


  private final class GemDrop(val x: Double, val y: Double, val gemId: Int) {
    var collected = false
  }

  private final class Sentry(
    val baseX: Double,
    val baseY: Double,
    val radius: Double,
    val pace: Double,
    val phase: Double,
    val tint: Color,
    val maxHealth: Int,
    val rank: Int
  ) {
    var x = baseX
    var y = baseY
    var disabled = 0.0
    var slowTime = 0.0
    var health = maxHealth
    var defeated = false
    var hitFlash = 0.0
  }

  private final case class NetPlayer(id: String, name: String, x: Double, y: Double, hp: Int, realm: Int)

  /** LAN room server. The room host owns the world; authenticated Admin clients may issue controller commands. */
  private final class LanServer(port: Int, hostName: String) {
    private val socket = new ServerSocket()
    private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
    private val counter = new AtomicInteger(0)
    private val writers = new ConcurrentHashMap[String, PrintWriter]()
    private val positions = new ConcurrentHashMap[String, String]()
    private val adminIds = new ConcurrentHashMap[String, String]()
    private val hostCommands = new ConcurrentLinkedQueue[String]()
    private val hostActions = new ConcurrentLinkedQueue[String]()
    @volatile private var roomRealm = 1
    @volatile private var roomDifficulty = "EASY"
    @volatile private var roomMode = "ADVENTURE"

    socket.setReuseAddress(true)
    socket.bind(new InetSocketAddress(port))
    positions.put("HOST", safeNetworkName(hostName) + ",135.0,1130.0,100,1")

    def start(): Unit = {
      val acceptThread = new Thread(new Runnable {
        override def run(): Unit = {
          while (running.get()) {
            try {
              val client = socket.accept()
              client.setTcpNoDelay(true)
              val worker = new Thread(new Runnable { override def run(): Unit = handleClient(client) })
              worker.setDaemon(true)
              worker.start()
            } catch {
              case _: java.net.SocketException => ()
              case _: Throwable => ()
            }
          }
        }
      })
      acceptThread.setDaemon(true)
      acceptThread.start()
    }

    private def handleClient(client: Socket): Unit = {
      var id = ""
      var output: PrintWriter = null
      try {
        val input = new BufferedReader(new InputStreamReader(client.getInputStream, "UTF-8"))
        output = new PrintWriter(client.getOutputStream, true)
        val first = input.readLine()
        if (first != null && first.startsWith("JOIN|")) {
          val fields = first.split("\\|", 3)
          val requestedName = safeNetworkName(if (fields.length > 1) fields(1) else "PLAYER")
          val token = if (fields.length > 2) fields(2) else ""
          id = "P" + counter.incrementAndGet()
          val authorized = requestedName.equalsIgnoreCase(ADMIN_NAME) && token == ADMIN_PIN
          writers.put(id, output)
          positions.put(id, requestedName + ",135.0,1130.0,100,1")
          if (authorized) adminIds.put(id, "1")
          output.println("WELCOME|" + id + "|" + (if (authorized) "ADMIN" else "PLAYER"))
          output.println("WORLD|" + roomRealm + "|" + roomDifficulty + "|" + roomMode)
          broadcastState()
          var line = input.readLine()
          while (line != null && running.get()) {
            if (line.startsWith("POS|")) {
              val values = line.substring(4).split(",", 4)
              if (values.length == 4) {
                try {
                  val old = positions.get(id)
                  val name = if (old == null) requestedName else old.takeWhile(_ != ',')
                  val px = clamp(values(0).toDouble, 0.0, WORLD_W)
                  val py = clamp(values(1).toDouble, 0.0, WORLD_H)
                  val hp = math.max(0, math.min(100, values(2).toInt))
                  val realm = math.max(1, math.min(realms.length, values(3).toInt))
                  positions.put(id, name + "," + px + "," + py + "," + hp + "," + realm)
                  broadcastState()
                } catch { case _: Throwable => () }
              }
            } else if (line.startsWith("ADMIN|") && adminIds.containsKey(id)) {
              val fields = line.split("\\|", 3)
              if (fields.length == 3) publishAdminCommand(fields(1), fields(2))
            } else if (line.startsWith("ACT|")) {
              val action = line.substring(4).take(100).replaceAll("[\\r\\n]", "_")
              hostActions.add(action)
              broadcastExcept(id, "ACT|" + action)
            }
            line = input.readLine()
          }
        }
      } catch { case _: Throwable => () }
      finally {
        if (id.nonEmpty) {
          writers.remove(id)
          positions.remove(id)
          adminIds.remove(id)
          broadcastState()
        }
        try client.close() catch { case _: Throwable => () }
      }
    }

    private def broadcast(message: String): Unit = {
      val it = writers.values().iterator()
      while (it.hasNext) {
        try { val out = it.next(); out.synchronized { out.println(message) } }
        catch { case _: Throwable => () }
      }
    }

    private def broadcastExcept(exceptId: String, message: String): Unit = {
      val it = writers.entrySet().iterator()
      while (it.hasNext) {
        val entry = it.next()
        if (entry.getKey != exceptId) {
          try { val out = entry.getValue; out.synchronized { out.println(message) } }
          catch { case _: Throwable => () }
        }
      }
    }

    private def broadcastState(): Unit = {
      val b = new StringBuilder("STATE|")
      val it = positions.entrySet().iterator()
      var first = true
      while (it.hasNext) {
        val e = it.next()
        if (!first) b.append(";")
        first = false
        b.append(e.getKey).append(",").append(e.getValue)
      }
      broadcast(b.toString)
    }

    private def publishAdminCommand(command: String, target: String): Unit = {
      val safe = command.replaceAll("[|;\\r\\n]", "_")
      val who = target.replaceAll("[|;\\r\\n]", "_")
      val wire = safe + "|" + who
      hostCommands.add(wire)
      broadcast("CMD|" + wire)
    }

    def publishHostPosition(x: Double, y: Double, hp: Int, realm: Int, difficulty: String, mode: String): Unit = {
      roomRealm = math.max(1, math.min(realms.length, realm))
      roomDifficulty = if (DIFFICULTIES.contains(difficulty)) difficulty else "EASY"
      roomMode = if (GAME_MODES.contains(mode)) mode else "ADVENTURE"
      positions.put("HOST", safeNetworkName(hostName) + "," + x + "," + y + "," + hp + "," + roomRealm)
      broadcastState()
      broadcast("WORLD|" + roomRealm + "|" + roomDifficulty + "|" + roomMode)
    }

    def broadcastHostCommand(command: String, target: String): Unit = publishAdminCommand(command, target)

    def consumeHostCommands(): Seq[String] = {
      val b = ArrayBuffer[String]()
      var value = hostCommands.poll()
      while (value != null) { b += value; value = hostCommands.poll() }
      b.toSeq
    }

    def broadcastHostAction(action: String): Unit = broadcast("ACT|" + action.take(100).replaceAll("[\\r\\n]", "_"))

    def consumeHostActions(): Seq[String] = {
      val b = ArrayBuffer[String]()
      var value = hostActions.poll()
      while (value != null) { b += value; value = hostActions.poll() }
      b.toSeq
    }

    def snapshot(): Seq[NetPlayer] = parseNetworkSnapshot(positions)

    def close(): Unit = {
      running.set(false)
      try socket.close() catch { case _: Throwable => () }
      val it = writers.values().iterator()
      while (it.hasNext) try it.next().close() catch { case _: Throwable => () }
      writers.clear(); positions.clear(); adminIds.clear(); hostActions.clear(); hostCommands.clear()
    }
  }

  /** LAN room client. Admin proof is only sent when the Admin has logged in locally. */
  private final class LanClient(host: String, port: Int, name: String, adminToken: String) {
    private val socket = new Socket()
    socket.connect(new InetSocketAddress(host, port), 3500)
    socket.setTcpNoDelay(true)
    private val output = new PrintWriter(socket.getOutputStream, true)
    private val input = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
    private val peers = new ConcurrentHashMap[String, String]()
    private val commands = new ConcurrentLinkedQueue[String]()
    private val actions = new ConcurrentLinkedQueue[String]()
    @volatile private var connected = true
    @volatile private var localId = ""
    @volatile private var serverConfirmedAdmin = false
    @volatile private var roomRealm = 1
    @volatile private var roomDifficulty = "EASY"
    @volatile private var roomMode = "ADVENTURE"

    output.println("JOIN|" + safeNetworkName(name) + "|" + adminToken)
    private val welcome = input.readLine()
    if (welcome == null || !welcome.startsWith("WELCOME|")) throw new java.io.IOException("The room did not accept the connection.")
    val welcomeParts = welcome.split("\\|", 3)
    localId = if (welcomeParts.length > 1) welcomeParts(1) else ""
    serverConfirmedAdmin = welcomeParts.length > 2 && welcomeParts(2) == "ADMIN"

    private val reader = new Thread(new Runnable {
      override def run(): Unit = {
        try {
          var line = input.readLine()
          while (line != null && connected) {
            if (line.startsWith("STATE|")) {
              peers.clear()
              val body = line.substring(6)
              if (body.nonEmpty) body.split(";").foreach { entry =>
                val parts = entry.split(",", 6)
                if (parts.length == 6) peers.put(parts(0), parts.drop(1).mkString(","))
              }
            } else if (line.startsWith("CMD|")) commands.add(line.substring(4))
            else if (line.startsWith("ACT|")) actions.add(line.substring(4))
            else if (line.startsWith("WORLD|")) {
              val cfg = line.split("\\|", 4)
              if (cfg.length == 4) {
                try {
                  val r = cfg(1).toInt
                  if (r >= 1 && r <= realms.length) roomRealm = r
                  if (DIFFICULTIES.contains(cfg(2))) roomDifficulty = cfg(2)
                  if (GAME_MODES.contains(cfg(3))) roomMode = cfg(3)
                } catch { case _: Throwable => () }
              }
            }
            line = input.readLine()
          }
        } catch { case _: Throwable => () }
        connected = false
      }
    })
    reader.setDaemon(true)
    reader.start()

    def id: String = localId
    def worldConfig: (Int, String, String) = (roomRealm, roomDifficulty, roomMode)
    def isAdminOnServer: Boolean = serverConfirmedAdmin
    def isConnected: Boolean = connected
    def sendPosition(x: Double, y: Double, hp: Int, realm: Int): Unit = if (connected) output.println("POS|" + x + "," + y + "," + hp + "," + realm)
    def sendAdminCommand(command: String, target: String): Unit = if (connected && serverConfirmedAdmin) output.println("ADMIN|" + command + "|" + target)
    def sendAction(action: String): Unit = if (connected) output.println("ACT|" + action.take(100).replaceAll("[\\r\\n]", "_"))
    def consumeActions(): Seq[String] = {
      val b = ArrayBuffer[String]()
      var value = actions.poll()
      while (value != null) { b += value; value = actions.poll() }
      b.toSeq
    }
    def consumeCommands(): Seq[String] = {
      val b = ArrayBuffer[String]()
      var value = commands.poll()
      while (value != null) { b += value; value = commands.poll() }
      b.toSeq
    }
    def snapshot(): Seq[NetPlayer] = parseNetworkSnapshot(peers)
    def close(): Unit = { connected = false; try socket.close() catch { case _: Throwable => () }; actions.clear(); commands.clear() }
  }

  private final class Bloom(val x: Double, val y: Double, val size: Double, val hue: Int, val phase: Double)
  private final class Star(val x: Double, val y: Double, val size: Int, val phase: Double)
  private final class Spark(var x: Double, var y: Double, var vx: Double, var vy: Double, val size: Double, val tint: Color, var life: Double)

  private def safeNetworkName(value: String): String = Option(value).getOrElse("PLAYER").trim.replaceAll("[,;|\\r\\n]", "_").take(18) match {
    case "" => "PLAYER"
    case other => other
  }

  private def parseNetworkSnapshot(source: ConcurrentHashMap[String, String]): Seq[NetPlayer] = {
    val result = ArrayBuffer[NetPlayer]()
    val it = source.entrySet().iterator()
    while (it.hasNext) {
      val e = it.next()
      val parts = e.getValue.split(",", 5)
      if (parts.length == 5) {
        try result += NetPlayer(e.getKey, parts(0), parts(1).toDouble, parts(2).toDouble, parts(3).toInt, parts(4).toInt)
        catch { case _: Throwable => () }
      }
    }
    result.toSeq
  }

  // ---------------------------------------------------------------------------
  // APPLICATION STATE
  // ---------------------------------------------------------------------------
  private var frame: JFrame = null
  private var root: JPanel = null
  private var cardLayout: java.awt.CardLayout = null
  private var gameCanvas: RealmCanvas = null
  private var gameTimer: Timer = null

  private var playerName = "PLAYER"
  private var isAdmin = false
  private var eventTitle = "SANCTUS EVENT"
  private var eventSubtitle = "THE LUMEN CIRCUIT"
  private var activeRealm = 1
  private var selectedDifficulty = "EASY"
  private var selectedMode = "ADVENTURE"
  private val completedRealms = mutable.Set[Int]()

  private val keys = mutable.Set[Int]()
  private val relics = ArrayBuffer[Relic]()
  private val sentries = ArrayBuffer[Sentry]()
  private val gemDrops = ArrayBuffer[GemDrop]()
  private val droppedGemIds = mutable.Set[Int]()
  private val collectedGems = mutable.Set[Int]()
  private val blooms = ArrayBuffer[Bloom]()
  private val stars = ArrayBuffer[Star]()
  private val sparks = ArrayBuffer[Spark]()
  private val rng = new java.util.Random(41042026L)

  private var playerX = SPAWN_X
  private var playerY = SPAWN_Y
  private var cameraX = 0.0
  private var cameraY = 0.0
  private var facingX = 0.0
  private var facingY = -1.0
  private var frameCount = 0L
  private var secondsLeft = 360.0
  private var charge = 4
  private var started = false
  private var paused = false
  private var cleared = false
  private var failed = false
  private var dashTime = 0.0
  private var dashCooldown = 0.0
  private var pulseCooldown = 0.0
  private var invulnerability = 0.0
  private var pulseFlash = 0.0
  private var playerHealth = 100
  private var attackCooldown = 0.0
  private var powerCooldown = 0.0
  private var attackFlash = 0.0
  private var speedBoostTime = 0.0
  private var equippedGem = -1
  private var enemyKills = 0
  private var multiplayerMode = "SOLO"
  private var lanServer: Option[LanServer] = None
  private var lanClient: Option[LanClient] = None
  private var lastNetworkFrame = 0L
  private var collectionFromGame = false
  private var collectionWasPaused = false
  private var adminFromGame = false
  private var adminWasPaused = false
  private var collectionButtons = ArrayBuffer[JButton]()
  private var collectionStatusArea: JTextArea = null
  private var adminTargetBox: JComboBox[String] = null
  private var toast = "Find the first enemy, fight with F, and claim its Ruby Core."
  private var toastTime = 4.0

  private var loginNameField: JTextField = null
  private var loginPinField: JPasswordField = null
  private var adminMenuButton: JButton = null
  private var welcomeLabel: JLabel = null
  private var realmButtons = ArrayBuffer[JButton]()
  private var adminTitleField: JTextField = null
  private var adminSubtitleField: JTextField = null
  private var adminDifficultyBox: JComboBox[String] = null
  private var adminModeBox: JComboBox[String] = null
  private var adminStatusArea: JTextArea = null
  private var hostButton: JButton = null
  private var joinButton: JButton = null
  private var networkAdminTimer: Timer = null

  // ---------------------------------------------------------------------------
  // GENERAL HELPERS
  // ---------------------------------------------------------------------------
  private def clamp(v: Double, lo: Double, hi: Double): Double = math.max(lo, math.min(hi, v))
  private def distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = {
    val dx = x1 - x2
    val dy = y1 - y2
    math.sqrt(dx * dx + dy * dy)
  }
  private def worldX(x: Double): Int = math.round(x - cameraX).toInt
  private def worldY(y: Double): Int = math.round(y - cameraY).toInt
  private def relicCount: Int = relics.count(_.collected)
  private def aliveEnemyCount: Int = sentries.count(s => !s.defeated)
  private def currentGem: Option[GemInfo] = POWER_GEMS.find(_.id == equippedGem)
  private def localNetworkId: String = if (multiplayerMode == "HOST") "HOST" else if (multiplayerMode == "JOIN") lanClient.map(_.id).getOrElse("") else "LOCAL"
  private def networkPlayers: Seq[NetPlayer] = multiplayerMode match {
    case "HOST" => lanServer.map(_.snapshot()).getOrElse(Seq.empty)
    case "JOIN" => lanClient.map(_.snapshot()).getOrElse(Seq.empty)
    case _ => Seq.empty
  }
  private def currentRealm: RealmInfo = realms(activeRealm - 1)
  private def colorFromHue(hue: Int, sat: Float = 0.68f, bright: Float = 1.0f): Color = Color.getHSBColor((hue.toDouble / 360.0).toFloat, sat, bright)
  private def setToast(message: String, duration: Double = 2.8): Unit = {
    toast = message
    toastTime = duration
  }

  private def makeButton(text: String): JButton = {
    val b = new JButton(text)
    b.setFont(new Font("Dialog", Font.BOLD, 14))
    b.setForeground(C_WHITE)
    b.setBackground(new Color(24, 59, 83))
    b.setFocusPainted(false)
    b.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR))
    b.setBorder(BorderFactory.createCompoundBorder(
      BorderFactory.createLineBorder(C_LINE, 1),
      BorderFactory.createEmptyBorder(10, 18, 10, 18)
    ))
    b.setAlignmentX(Component.CENTER_ALIGNMENT)
    b
  }

  private def makeTitle(text: String, size: Int, color: Color): JLabel = {
    val l = new JLabel(text, SwingConstants.CENTER)
    l.setFont(new Font("Dialog", Font.BOLD, size))
    l.setForeground(color)
    l.setAlignmentX(Component.CENTER_ALIGNMENT)
    l
  }

  private def makeLabel(text: String, size: Int = 13, color: Color = C_SOFT): JLabel = {
    val l = new JLabel(text, SwingConstants.CENTER)
    l.setFont(new Font("Dialog", Font.PLAIN, size))
    l.setForeground(color)
    l.setAlignmentX(Component.CENTER_ALIGNMENT)
    l
  }

  private def verticalPanel(): JPanel = {
    val p = new JPanel()
    p.setOpaque(true)
    p.setBackground(C_BG)
    p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS))
    p.setBorder(BorderFactory.createEmptyBorder(22, 28, 22, 28))
    p
  }

  private def addCentered(p: JPanel, c: JComponent, gap: Int = 9): Unit = {
    p.add(Box.createVerticalStrut(gap))
    c.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(c)
  }

  private def addAction(button: JButton)(action: => Unit): Unit = {
    button.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = action
    })
  }

  private def makeCombo(values: Array[String], selected: String): JComboBox[String] = {
    val c = new JComboBox[String](values)
    c.setSelectedItem(selected)
    c.setFont(new Font("Dialog", Font.BOLD, 13))
    c.setForeground(C_WHITE)
    c.setBackground(C_PANEL_2)
    c
  }

  private def showCard(name: String): Unit = {
    cardLayout.show(root, name)
    root.revalidate()
    root.repaint()
  }

  private def scrollCard(panel: JPanel): JScrollPane = {
    val s = new JScrollPane(panel)
    s.setBorder(BorderFactory.createEmptyBorder())
    s.getViewport.setBackground(C_BG)
    s.getVerticalScrollBar.setUnitIncrement(18)
    s
  }

  // ---------------------------------------------------------------------------
  // LEVEL CONSTRUCTION AND GAME FLOW
  // ---------------------------------------------------------------------------
  private def startRealm(number: Int, bypassUnlock: Boolean = false): Unit = {
    if (number < 1 || number > realms.length) return
    if (!bypassUnlock && number > 1 && !completedRealms.contains(number - 1)) {
      JOptionPane.showMessageDialog(frame, "Clear the previous realm before opening this gate.", "Gate sealed", JOptionPane.INFORMATION_MESSAGE)
      return
    }
    activeRealm = number
    buildRealm()
    if (gameTimer != null) gameTimer.stop()
    if (gameCanvas != null) root.remove(gameCanvas)
    gameCanvas = new RealmCanvas()
    root.add(gameCanvas, "game")
    showCard("game")
    gameTimer = new Timer(16, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        updateGame(0.016)
        if (gameCanvas != null) gameCanvas.repaint()
      }
    })
    gameTimer.start()
    gameCanvas.requestFocusInWindow()
  }

  private def buildRealm(): Unit = {
    relics.clear()
    sentries.clear()
    gemDrops.clear()
    droppedGemIds.clear()
    blooms.clear()
    stars.clear()
    sparks.clear()
    keys.clear()

    val info = currentRealm
    val difficultyIndex = DIFFICULTIES.indexOf(selectedDifficulty) match {
      case n if n >= 0 => n
      case _ => 1
    }
    val mode = selectedMode
    val extraRelics = if (mode == "GEM HUNT") 2 else 0
    val totalRelics = info.relics + extraRelics

    var i = 0
    while (i < totalRelics) {
      val x = 285.0 + ((i * 271 + activeRealm * 113) % 1310).toDouble
      val y = 260.0 + ((i * 187 + activeRealm * 79) % 810).toDouble
      val tint = colorFromHue((info.hue + i * 39 + 35) % 360)
      val name: String = RELIC_NAMES(i % RELIC_NAMES.length)
      relics += new Relic(x, y, tint, name)
      i += 1
    }

    val noHazards = mode == "FREE ROAM"
    val hazardCount = if (noHazards) 0 else math.max(1, info.sentries + math.max(0, difficultyIndex - 1))
    i = 0
    while (i < hazardCount) {
      val x = 470.0 + i * 330.0
      val y = 345.0 + (i % 2) * 390.0
      val tint = if (i % 2 == 0) C_PINK else C_VIOLET
      val hp = 46 + (activeRealm - 1) * 18 + difficultyIndex * 17 + i * 8
      val rank = 1 + activeRealm + difficultyIndex
      val pace = 0.58 + (activeRealm - 1) * 0.13 + difficultyIndex * 0.11
      sentries += new Sentry(x, y, 75.0 + difficultyIndex * 15.0, pace, i * 1.31, tint, hp, rank)
      i += 1
    }

    i = 0
    while (i < 250) {
      val x = 25.0 + rng.nextDouble() * (WORLD_W - 50.0)
      val y = 30.0 + rng.nextDouble() * (WORLD_H - 60.0)
      blooms += new Bloom(x, y, 3.0 + rng.nextDouble() * 8.0, (info.hue + rng.nextInt(130)) % 360, rng.nextDouble() * 6.28)
      i += 1
    }

    i = 0
    while (i < 180) {
      stars += new Star(rng.nextDouble() * WORLD_W, rng.nextDouble() * WORLD_H, 1 + rng.nextInt(3), rng.nextDouble() * 6.28)
      i += 1
    }

    playerX = SPAWN_X
    playerY = SPAWN_Y
    cameraX = 0.0
    cameraY = 0.0
    facingX = 0.0
    facingY = -1.0
    frameCount = 0L
    val difficultyTime: Int = DIFFICULTY_TIME(difficultyIndex)
    val modeTime = if (mode == "TIME ATTACK") -100 else if (mode == "GEM HUNT") -35 else 0
    secondsLeft = if (mode == "FREE ROAM") 99999.0 else math.max(100.0, info.time + difficultyTime + modeTime)
    charge = if (difficultyIndex >= 3) 3 else 4
    playerHealth = 100
    attackCooldown = 0.0
    powerCooldown = 0.0
    attackFlash = 0.0
    speedBoostTime = 0.0
    enemyKills = 0
    if (equippedGem < 0 && collectedGems.nonEmpty) equippedGem = collectedGems.toSeq.sorted.head
    started = true
    paused = false
    cleared = false
    failed = false
    dashTime = 0.0
    dashCooldown = 0.0
    pulseCooldown = 0.0
    invulnerability = 0.0
    pulseFlash = 0.0
    setToast("Realm opened: " + info.name + " | Fight with F. First enemy drops the Ruby Core.", 5.0)
  }

  private def stopGame(): Unit = {
    if (gameTimer != null) gameTimer.stop()
    gameTimer = null
    started = false
    keys.clear()
  }

  private def returnToRealms(): Unit = {
    stopGame()
    refreshRealmButtons()
    showCard("realms")
  }

  private def restartRealm(): Unit = {
    buildRealm()
    if (gameCanvas != null) gameCanvas.requestFocusInWindow()
  }

  private def dropGem(enemy: Sentry): Unit = {
    val next = POWER_GEMS.find(g => !collectedGems.contains(g.id) && !droppedGemIds.contains(g.id))
    next.foreach { gem =>
      droppedGemIds += gem.id
      gemDrops += new GemDrop(enemy.x, enemy.y, gem.id)
      burst(enemy.x, enemy.y, gem.tint, 25)
      setToast("ENEMY DEFEATED ? " + gem.name + " dropped! Move close and press E.", 3.8)
    }
  }

  private def defeatEnemy(enemy: Sentry): Unit = {
    if (!enemy.defeated) {
      enemy.defeated = true
      enemyKills += 1
      dropGem(enemy)
      burst(enemy.x, enemy.y, enemy.tint, 32)
    }
  }

  private def damageEnemy(enemy: Sentry, amount: Int): Unit = {
    if (!enemy.defeated) {
      enemy.health = math.max(0, enemy.health - amount)
      enemy.hitFlash = 0.22
      burst(enemy.x, enemy.y, enemy.tint, 7)
      if (enemy.health <= 0) defeatEnemy(enemy)
    }
  }

  private def fight(): Unit = {
    if (!started || paused || cleared || failed || attackCooldown > 0.0) return
    attackCooldown = 0.42
    attackFlash = 0.20
    val target = sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 132.0)
      .sortBy(s => distance(playerX, playerY, s.x, s.y)).headOption
    target match {
      case Some(enemy) =>
        val damage = 34 + activeRealm * 3 + (if (speedBoostTime > 0.0) 8 else 0)
        sharedDamageEnemy(enemy, damage)
        if (enemy.defeated) setToast("ENEMY DEFEATED ? a POWER GEM has dropped. Move close and press E.", 3.0)
        else setToast("GAUNTLET STRIKE  ?  " + damage + " DAMAGE  ?  ENEMY HP " + enemy.health + "/" + enemy.maxHealth, 1.5)
      case None => setToast("No enemy in range. Move closer, then press F to fight.", 1.5)
    }
  }

  private def useEquippedPower(): Unit = {
    if (!started || paused || cleared || failed) return
    if (equippedGem < 0 || !collectedGems.contains(equippedGem)) {
      setToast("NO GEM EQUIPPED ? defeat an enemy and collect the Ruby Core first.", 2.5)
      return
    }
    if (powerCooldown > 0.0) {
      setToast("POWER RECHARGING: " + math.ceil(powerCooldown).toInt + "s", 1.3)
      return
    }
    powerCooldown = 7.0
    val gem = POWER_GEMS(equippedGem)
    gem.id match {
      case 0 =>
        var hits = 0
        sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 300.0).foreach { enemy =>
          sharedDamageEnemy(enemy, 55 + activeRealm * 7); hits += 1
        }
        burst(playerX + facingX * 70.0, playerY + facingY * 70.0, gem.tint, 36)
        setToast("RUBY CORE  ?  FLAME ARC  ?  " + hits + " ENEMY/ENEMIES HIT", 2.2)
      case 1 =>
        var frozen = 0
        sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 460.0).foreach { enemy => enemy.disabled = 4.5; sendSharedAction("FREEZE|" + sentries.indexOf(enemy) + "|4500"); frozen += 1 }
        burst(playerX, playerY, gem.tint, 42)
        setToast("SAPPHIRE CORE  ?  FROST LOCK  ?  " + frozen + " ENEMY/ENEMIES FROZEN", 2.2)
      case 2 =>
        playerHealth = math.min(100, playerHealth + 42)
        charge = math.min(4, charge + 1)
        burst(playerX, playerY, gem.tint, 42)
        setToast("EMERALD CORE  ?  LIFE BLOOM  ?  HEALTH RESTORED", 2.2)
      case 3 =>
        var hits = 0
        sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 360.0).foreach { enemy =>
          sharedDamageEnemy(enemy, 76 + activeRealm * 8); hits += 1
        }
        burst(playerX, playerY, gem.tint, 65)
        setToast("AMETHYST CORE  ?  NOVA BURST  ?  " + hits + " ENEMY/ENEMIES HIT", 2.2)
      case 4 =>
        sentries.filterNot(_.defeated).foreach { enemy => enemy.slowTime = 6.0 }; sendSharedAction("SLOW|6000")
        burst(playerX, playerY, gem.tint, 55)
        setToast("TOPAZ CORE  ?  TIME BEND  ?  ALL ENEMIES SLOWED", 2.2)
      case _ => ()
    }
  }

  private def equipGem(id: Int): Unit = {
    if (id >= 0 && id < POWER_GEMS.length && collectedGems.contains(id)) {
      equippedGem = id
      val gem = POWER_GEMS(id)
      setToast("EQUIPPED: " + gem.name + "  ?  PRESS R TO USE " + gem.powerName, 2.4)
      refreshCollection()
    } else setToast("That gem has not been collected yet.", 1.6)
  }

  private def showCollection(fromGame: Boolean): Unit = {
    collectionFromGame = fromGame
    collectionWasPaused = paused
    if (fromGame) paused = true
    refreshCollection()
    showCard("collection")
  }

  private def returnFromCollection(): Unit = {
    if (collectionFromGame) {
      paused = collectionWasPaused
      showCard("game")
      if (gameCanvas != null) gameCanvas.requestFocusInWindow()
    } else showCard("menu")
  }

  private def openAdminPanel(fromGame: Boolean): Unit = {
    adminFromGame = fromGame
    adminWasPaused = paused
    if (fromGame) paused = true
    refreshAdmin()
    showCard("admin")
  }

  private def closeAdminPanel(): Unit = {
    if (adminFromGame) {
      paused = adminWasPaused
      showCard("game")
      if (gameCanvas != null) gameCanvas.requestFocusInWindow()
    } else showCard("menu")
    adminFromGame = false
  }

  private def sendSharedAction(action: String): Unit = {
    multiplayerMode match {
      case "HOST" => lanServer.foreach(_.broadcastHostAction(action))
      case "JOIN" => lanClient.foreach(_.sendAction(action))
      case _ => ()
    }
  }

  private def sharedDamageEnemy(enemy: Sentry, amount: Int): Unit = {
    val index = sentries.indexOf(enemy)
    damageEnemy(enemy, amount)
    if (index >= 0) sendSharedAction("HIT|" + index + "|" + amount)
  }

  private def collectGemId(id: Int): Unit = {
    if (id >= 0 && id < POWER_GEMS.length && !collectedGems.contains(id)) {
      collectedGems += id
      droppedGemIds -= id
      gemDrops.find(_.gemId == id).foreach(drop => drop.collected = true)
      if (equippedGem < 0) equippedGem = id
      val gem = POWER_GEMS(id)
      setToast("POWER GEM COLLECTED: " + gem.name + "  ?  PRESS " + (id + 1) + " TO EQUIP.", 3.4)
      refreshCollection()
    }
  }

  private def applySharedAction(wire: String): Unit = {
    val parts = wire.split("\\|", 4)
    if (parts.length >= 2) {
      parts(0) match {
        case "HIT" if parts.length >= 3 =>
          try {
            val idx = parts(1).toInt
            val amount = parts(2).toInt
            if (idx >= 0 && idx < sentries.length && amount >= 1 && amount <= 250) damageEnemy(sentries(idx), amount)
          } catch { case _: Throwable => () }
        case "GEM" => try collectGemId(parts(1).toInt) catch { case _: Throwable => () }
        case "SIGIL" =>
          try { val idx = parts(1).toInt; if (idx >= 0 && idx < relics.length) relics(idx).collected = true }
          catch { case _: Throwable => () }
        case "FREEZE" =>
          try {
            val idx = parts(1).toInt
            val rawDuration = if (parts.length >= 3) parts(2).toDouble / 1000.0 else 2.7
            val duration = clamp(rawDuration, 0.0, 12.0)
            if (idx >= 0 && idx < sentries.length && !sentries(idx).defeated)
              sentries(idx).disabled = math.max(sentries(idx).disabled, duration)
          } catch { case _: Throwable => () }
        case "SLOW" =>
          try {
            val duration = clamp(parts(1).toDouble / 1000.0, 0.0, 12.0)
            sentries.filterNot(_.defeated).foreach(enemy => enemy.slowTime = math.max(enemy.slowTime, duration))
          } catch { case _: Throwable => () }
        case _ => ()
      }
    }
  }

  private def consumeNetworkActions(): Unit = {
    val actions = if (multiplayerMode == "HOST") lanServer.map(_.consumeHostActions()).getOrElse(Seq.empty)
                  else if (multiplayerMode == "JOIN") lanClient.map(_.consumeActions()).getOrElse(Seq.empty)
                  else Seq.empty
    actions.foreach(applySharedAction)
  }

  private def applyAdminAction(command: String): Unit = {
    command match {
      case "HEAL" =>
        playerHealth = 100
        charge = math.min(4, math.max(1, charge))
        failed = false
        setToast("ADMIN CONTROL: HEALTH RESTORED.", 2.4)
      case "GEMS" =>
        POWER_GEMS.foreach(g => collectedGems += g.id)
        if (equippedGem < 0) equippedGem = 0
        refreshCollection()
        setToast("ADMIN CONTROL: ALL FIVE POWER GEMS GRANTED.", 2.4)
      case "BOOST" =>
        speedBoostTime = 12.0
        dashCooldown = 0.0
        setToast("ADMIN CONTROL: SPEED BOOST ENABLED.", 2.4)
      case "FREEZE" =>
        sentries.filterNot(_.defeated).foreach(enemy => enemy.disabled = 10.0)
        setToast("ADMIN CONTROL: ENEMIES FROZEN.", 2.4)
      case "CLEAR" =>
        sentries.foreach(s => if (!s.defeated) { s.health = 0; defeatEnemy(s) })
        setToast("ADMIN CONTROL: ENEMY WAVE CLEARED.", 2.4)
      case "REVIVE" =>
        playerHealth = 100
        charge = 4
        failed = false
        paused = false
        invulnerability = 2.5
        started = true
        setToast("ADMIN CONTROL: PLAYER REVIVED.", 2.4)
      case _ => setToast("Unknown Admin command.", 1.5)
    }
  }

  private def sendAdminAction(command: String): Unit = {
    if (!isAdmin) return
    val choice = if (adminTargetBox == null || adminTargetBox.getSelectedItem == null) "ALL" else adminTargetBox.getSelectedItem.toString
    val target = if (choice == "ALL PLAYERS") "ALL" else if (choice.startsWith("HOST  ?")) "HOST" else if (choice.contains("  ?  ")) choice.takeWhile(_ != ' ') else "ALL"
    multiplayerMode match {
      case "HOST" => lanServer match {
        case Some(server) => server.broadcastHostCommand(command, target)
        case None => applyAdminAction(command)
      }
      case "JOIN" => lanClient match {
        case Some(client) if client.isAdminOnServer => client.sendAdminCommand(command, target)
        case _ => JOptionPane.showMessageDialog(frame, "Admin multiplayer controls require an authorized room connection.", "Admin access", JOptionPane.WARNING_MESSAGE)
      }
      case _ => applyAdminAction(command)
    }
    refreshAdminTargets()
  }

  private def consumeNetworkAdminCommands(): Unit = {
    val commands = if (multiplayerMode == "HOST") lanServer.map(_.consumeHostCommands()).getOrElse(Seq.empty)
                   else if (multiplayerMode == "JOIN") lanClient.map(_.consumeCommands()).getOrElse(Seq.empty)
                   else Seq.empty
    commands.foreach { wire =>
      val parts = wire.split("\\|", 2)
      if (parts.length == 2) {
        val target = parts(1)
        val local = target == "ALL" || target == localNetworkId
        if (local) applyAdminAction(parts(0))
      }
    }
  }

  private def hostLanGame(): Unit = {
    val entered = JOptionPane.showInputDialog(frame, "Room port (default 5050):", String.valueOf(NETWORK_PORT))
    if (entered == null) return
    try {
      val port = entered.trim.toInt
      require(port >= 1024 && port <= 65535, "Port must be between 1024 and 65535.")
      closeNetwork()
      val server = new LanServer(port, playerName)
      server.start()
      lanServer = Some(server)
      multiplayerMode = "HOST"
      startRealm(1, true)
      val hostIp = try InetAddress.getLocalHost.getHostAddress catch { case _: Throwable => "Find this PC's IPv4 address in Network Settings" }
      JOptionPane.showMessageDialog(frame, "LAN room is running.\nHost address: " + hostIp + "\nPort: " + port + "\nOther players must use the same Wi-Fi or hotspot.", "Room hosted", JOptionPane.INFORMATION_MESSAGE)
      setToast("LAN ROOM HOSTED  ?  " + hostIp + ":" + port, 5.0)
    } catch {
      case ex: Throwable => JOptionPane.showMessageDialog(frame, "Could not host LAN room: " + ex.getMessage, "Room host error", JOptionPane.ERROR_MESSAGE)
    }
  }

  private def joinLanGame(): Unit = {
    val host = JOptionPane.showInputDialog(frame, "Host computer IP address:", "192.168.1.2")
    if (host == null || host.trim.isEmpty) return
    val entered = JOptionPane.showInputDialog(frame, "Room port:", String.valueOf(NETWORK_PORT))
    if (entered == null) return
    try {
      val port = entered.trim.toInt
      require(port >= 1024 && port <= 65535, "Port must be between 1024 and 65535.")
      closeNetwork()
      val token = if (isAdmin) ADMIN_PIN else ""
      val client = new LanClient(host.trim, port, playerName, token)
      lanClient = Some(client)
      multiplayerMode = "JOIN"
      if (isAdmin && !client.isAdminOnServer) JOptionPane.showMessageDialog(frame, "This room did not authorize the Admin account. You can play, but cannot control other players.", "Admin access", JOptionPane.INFORMATION_MESSAGE)
      startRealm(1, true)
      setToast("CONNECTED TO LAN ROOM  ?  " + host.trim + ":" + port, 4.0)
    } catch {
      case ex: Throwable => JOptionPane.showMessageDialog(frame, "Could not join LAN room: " + ex.getMessage, "Join error", JOptionPane.ERROR_MESSAGE)
    }
  }

  private def closeNetwork(): Unit = {
    lanClient.foreach(_.close()); lanClient = None
    lanServer.foreach(_.close()); lanServer = None
    multiplayerMode = "SOLO"
  }

  private def doPulse(): Unit = {
    if (!started || paused || cleared || failed || pulseCooldown > 0.0) return
    pulseCooldown = 4.2
    pulseFlash = 0.55
    var affected = 0
    sentries.foreach { s =>
      if (!s.defeated && distance(playerX, playerY, s.x, s.y) < 245.0) {
        s.disabled = 2.7
        sendSharedAction("FREEZE|" + sentries.indexOf(s) + "|2700")
        affected += 1
        burst(s.x, s.y, C_BLUE, 13)
      }
    }
    setToast(if (affected > 0) "LUMEN PULSE RELEASED ? nearby sentries paused." else "LUMEN PULSE RELEASED.", 2.0)
  }

  private def interact(): Unit = {
    if (!started || paused || cleared || failed) return
    val nearbyGem = gemDrops.filterNot(_.collected).sortBy(d => distance(playerX, playerY, d.x, d.y)).headOption
    nearbyGem match {
      case Some(drop) if distance(playerX, playerY, drop.x, drop.y) < 96.0 =>
        val gemId = drop.gemId
        val gem = POWER_GEMS(gemId)
        drop.collected = true
        collectGemId(gemId)
        sendSharedAction("GEM|" + gemId)
        burst(drop.x, drop.y, gem.tint, 35)
      case _ =>
        val nearest = relics.filterNot(_.collected).sortBy(r => distance(playerX, playerY, r.x, r.y)).headOption
        nearest match {
          case Some(r) if distance(playerX, playerY, r.x, r.y) < 94.0 =>
            r.collected = true
            sendSharedAction("SIGIL|" + relics.indexOf(r))
            burst(r.x, r.y, r.tint, 25)
            setToast("ACQUIRED: " + r.title + "   [" + relicCount + "/" + relics.length + "]", 2.2)
            if (relicCount == relics.length) setToast("ALL SIGILS SECURED ? travel to the far exit arch.", 4.0)
          case _ =>
            if (distance(playerX, playerY, PORTAL_X, PORTAL_Y) < 135.0) {
              if (relicCount == relics.length && aliveEnemyCount == 0) {
                cleared = true
                completedRealms += activeRealm
                burst(PORTAL_X, PORTAL_Y, C_GOLD, 80)
                setToast("REALM CLEARED ? " + currentRealm.name, 6.0)
              } else {
                val missing = relics.length - relicCount
                val remainingEnemies = aliveEnemyCount
                setToast("EXIT SEALED ? SIGILS LEFT: " + missing + "  ?  ENEMIES LEFT: " + remainingEnemies, 2.8)
              }
            } else setToast("Move close to a dropped gem, sigil, or exit arch, then press E.", 2.1)
        }
    }
  }

  private def burst(x: Double, y: Double, tint: Color, amount: Int): Unit = {
    var i = 0
    while (i < amount) {
      val a = rng.nextDouble() * math.Pi * 2.0
      val speed = 35.0 + rng.nextDouble() * 150.0
      sparks += new Spark(x, y, math.cos(a) * speed, math.sin(a) * speed, 2.0 + rng.nextDouble() * 4.0, tint, 0.65 + rng.nextDouble() * 1.2)
      i += 1
    }
  }

  private def updateGame(dt: Double): Unit = {
    frameCount += 1
    if (toastTime > 0.0) toastTime = math.max(0.0, toastTime - dt)
    if (dashTime > 0.0) dashTime = math.max(0.0, dashTime - dt)
    if (dashCooldown > 0.0) dashCooldown = math.max(0.0, dashCooldown - dt)
    if (pulseCooldown > 0.0) pulseCooldown = math.max(0.0, pulseCooldown - dt)
    if (invulnerability > 0.0) invulnerability = math.max(0.0, invulnerability - dt)
    if (pulseFlash > 0.0) pulseFlash = math.max(0.0, pulseFlash - dt)
    if (attackCooldown > 0.0) attackCooldown = math.max(0.0, attackCooldown - dt)
    if (powerCooldown > 0.0) powerCooldown = math.max(0.0, powerCooldown - dt)
    if (attackFlash > 0.0) attackFlash = math.max(0.0, attackFlash - dt)
    if (speedBoostTime > 0.0) speedBoostTime = math.max(0.0, speedBoostTime - dt)

    sparks.foreach { p =>
      p.x += p.vx * dt
      p.y += p.vy * dt
      p.vx *= 0.985
      p.vy *= 0.985
      p.life -= dt
    }
    sparks.filter(_.life <= 0.0).foreach(p => sparks -= p)
    while (sparks.length < 95) {
      sparks += new Spark(rng.nextDouble() * WORLD_W, WORLD_H - 8.0, (rng.nextDouble() - 0.5) * 10.0, -8.0 - rng.nextDouble() * 16.0, 1.5 + rng.nextDouble() * 2.4, C_MINT, 4.0)
    }

    sentries.foreach { s =>
      if (s.disabled > 0.0) s.disabled = math.max(0.0, s.disabled - dt)
      if (s.slowTime > 0.0) s.slowTime = math.max(0.0, s.slowTime - dt)
      if (s.hitFlash > 0.0) s.hitFlash = math.max(0.0, s.hitFlash - dt)
      val slowFactor = if (s.slowTime > 0.0) 0.20 else 1.0
      val t = frameCount * 0.021 * s.pace * slowFactor + s.phase
      // A frozen enemy really stops moving; a slowed enemy moves at 20% pace.
      if (!s.defeated && s.disabled <= 0.0) {
        s.x = s.baseX + math.sin(t) * s.radius
        s.y = s.baseY + math.cos(t * 0.72) * 34.0
      }
    }

    consumeNetworkAdminCommands()
    consumeNetworkActions()
    if (multiplayerMode == "HOST" && frameCount % 6 == 0L) {
      lanServer.foreach(_.publishHostPosition(playerX, playerY, playerHealth, activeRealm, selectedDifficulty, selectedMode))
    } else if (multiplayerMode == "JOIN" && frameCount % 6 == 0L) {
      lanClient.foreach(_.sendPosition(playerX, playerY, playerHealth, activeRealm))
      lanClient.foreach { client =>
        val config = client.worldConfig
        if (config._1 != activeRealm || config._2 != selectedDifficulty || config._3 != selectedMode) {
          activeRealm = config._1
          selectedDifficulty = config._2
          selectedMode = config._3
          buildRealm()
          setToast("ROOM SETTINGS UPDATED ? REALM " + activeRealm + ".", 3.0)
        }
      }
    }

    if (!started || paused || cleared || failed) return

    if (selectedMode != "FREE ROAM") {
      secondsLeft = math.max(0.0, secondsLeft - dt)
      if (secondsLeft <= 0.0) {
        failed = true
        setToast("TIME WINDOW CLOSED ? press T to retry this realm.", 6.0)
        return
      }
    }

    var mx = 0.0
    var my = 0.0
    if (keys.contains(KeyEvent.VK_W) || keys.contains(KeyEvent.VK_UP)) my -= 1.0
    if (keys.contains(KeyEvent.VK_S) || keys.contains(KeyEvent.VK_DOWN)) my += 1.0
    if (keys.contains(KeyEvent.VK_A) || keys.contains(KeyEvent.VK_LEFT)) mx -= 1.0
    if (keys.contains(KeyEvent.VK_D) || keys.contains(KeyEvent.VK_RIGHT)) mx += 1.0
    val length = math.sqrt(mx * mx + my * my)
    if (length > 0.0) {
      mx /= length
      my /= length
      facingX = mx
      facingY = my
      val speed = if (dashTime > 0.0) 465.0 else if (speedBoostTime > 0.0) 315.0 else 225.0
      playerX = clamp(playerX + mx * speed * dt, 35.0, WORLD_W - 35.0)
      playerY = clamp(playerY + my * speed * dt, 35.0, WORLD_H - 35.0)
    }

    sentries.foreach { enemy =>
      if (!enemy.defeated && enemy.disabled <= 0.0 && invulnerability <= 0.0 && distance(playerX, playerY, enemy.x, enemy.y) < 45.0) {
        playerHealth -= 18 + activeRealm * 2
        invulnerability = 0.90
        playerX = clamp(playerX - facingX * 54.0, 35.0, WORLD_W - 35.0)
        playerY = clamp(playerY - facingY * 54.0, 35.0, WORLD_H - 35.0)
        burst(playerX, playerY, C_PINK, 13)
        setToast("ENEMY HIT ? HEALTH " + math.max(0, playerHealth) + "/100", 1.7)
        if (playerHealth <= 0) {
          charge -= 1
          if (charge > 0) { playerHealth = 62; invulnerability = 2.0; setToast("CHARGE USED ? " + charge + " CHARGE(S) LEFT.", 2.4) }
          else { failed = true; setToast("NO CHARGES LEFT ? press T to retry this realm.", 5.0) }
        }
      }
    }

  }

  // ---------------------------------------------------------------------------
  // REAL-TIME PERSPECTIVE 3D RENDERER (software-rendered polygons; no libraries)
  // ---------------------------------------------------------------------------
  private final case class V3(x: Double, y: Double, z: Double)
  private final case class P3(x: Int, y: Int, depth: Double)
  private final case class Face3(points: Vector[V3], fill: Color, edge: Color)

  private var cameraDistanceIndex = 1
  private val CAMERA_DISTANCES: Vector[Double] = Vector(340.0, 470.0, 630.0)
  private val CAMERA_LABELS: Vector[String] = Vector("CLOSE", "FOLLOW", "WIDE")

  private def cameraForward: (Double, Double) = {
    val n = math.sqrt(facingX * facingX + facingY * facingY)
    if (n < 0.0001) (0.0, -1.0) else (facingX / n, facingY / n)
  }

  /** Perspective projection from world X/Y plus vertical Z into a third-person camera. */
  private def project3D(v: V3, w: Int, h: Int): Option[P3] = {
    val f = cameraForward
    val fx = f._1
    val fy = f._2
    val rx = -fy
    val ry = fx
    val camDistance = CAMERA_DISTANCES(cameraDistanceIndex)
    val camX = playerX - fx * camDistance
    val camY = playerY - fy * camDistance
    val camZ = camDistance * 0.53
    val dx = v.x - camX
    val dy = v.y - camY
    val dz = v.z - camZ
    val along = dx * fx + dy * fy
    val side = dx * rx + dy * ry
    val sinTilt = 0.34
    val cosTilt = 0.941
    val depth = along * cosTilt - dz * sinTilt
    if (depth <= 34.0 || depth > 5200.0) None
    else {
      val vertical = along * sinTilt + dz * cosTilt
      val focal = math.min(w.toDouble, h.toDouble) * 0.92
      val sx = w * 0.5 + side * focal / depth
      val sy = h * 0.49 - vertical * focal / depth
      if (sx < -w * 2.5 || sx > w * 3.5 || sy < -h * 3.0 || sy > h * 3.5) None
      else Some(P3(sx.toInt, sy.toInt, depth))
    }
  }

  private def shade3D(c: Color, factor: Double): Color = {
    val r = math.max(0, math.min(255, (c.getRed * factor).toInt))
    val g = math.max(0, math.min(255, (c.getGreen * factor).toInt))
    val b = math.max(0, math.min(255, (c.getBlue * factor).toInt))
    new Color(r, g, b, c.getAlpha)
  }

  private def addBox3D(out: ArrayBuffer[Face3], x: Double, y: Double, z: Double,
                       width: Double, depth: Double, height: Double, base: Color): Unit = {
    val hx = width * 0.5
    val hy = depth * 0.5
    val a = V3(x - hx, y - hy, z)
    val b = V3(x + hx, y - hy, z)
    val c = V3(x + hx, y + hy, z)
    val d = V3(x - hx, y + hy, z)
    val A = V3(x - hx, y - hy, z + height)
    val B = V3(x + hx, y - hy, z + height)
    val C = V3(x + hx, y + hy, z + height)
    val D = V3(x - hx, y + hy, z + height)
    val edge = shade3D(base, 0.48)
    out += Face3(Vector(A, B, C, D), shade3D(base, 1.34), edge)
    out += Face3(Vector(a, b, B, A), shade3D(base, 0.88), edge)
    out += Face3(Vector(b, c, C, B), shade3D(base, 0.70), edge)
    out += Face3(Vector(c, d, D, C), shade3D(base, 0.82), edge)
    out += Face3(Vector(d, a, A, D), shade3D(base, 1.02), edge)
    out += Face3(Vector(d, c, b, a), shade3D(base, 0.48), edge)
  }

  private def addDisc3D(out: ArrayBuffer[Face3], x: Double, y: Double, z: Double,
                        rx: Double, ry: Double, tint: Color): Unit = {
    var i = 0
    val segments = 14
    while (i < segments) {
      val a0 = i * math.Pi * 2.0 / segments
      val a1 = (i + 1) * math.Pi * 2.0 / segments
      val center = V3(x, y, z)
      val p0 = V3(x + math.cos(a0) * rx, y + math.sin(a0) * ry, z)
      val p1 = V3(x + math.cos(a1) * rx, y + math.sin(a1) * ry, z)
      out += Face3(Vector(center, p0, p1), tint, new Color(12, 24, 40, 0))
      i += 1
    }
  }

  private def fog3D(c: Color, depth: Double): Color = {
    val amount = clamp((depth - 760.0) / 3000.0, 0.0, 0.48)
    val fog = new Color(38, 76, 103)
    val r = (c.getRed * (1.0 - amount) + fog.getRed * amount).toInt
    val g = (c.getGreen * (1.0 - amount) + fog.getGreen * amount).toInt
    val b = (c.getBlue * (1.0 - amount) + fog.getBlue * amount).toInt
    new Color(math.max(0, math.min(255, r)), math.max(0, math.min(255, g)), math.max(0, math.min(255, b)), c.getAlpha)
  }

  private def addOcta3D(out: ArrayBuffer[Face3], x: Double, y: Double, z: Double,
                        radius: Double, height: Double, tint: Color): Unit = {
    val top = V3(x, y, z + height)
    val bottom = V3(x, y, z - height * 0.72)
    val left = V3(x - radius, y, z)
    val right = V3(x + radius, y, z)
    val near = V3(x, y - radius, z)
    val far = V3(x, y + radius, z)
    val edge = shade3D(tint, 0.43)
    out += Face3(Vector(top, near, right), shade3D(tint, 1.28), edge)
    out += Face3(Vector(top, right, far), shade3D(tint, 0.94), edge)
    out += Face3(Vector(top, far, left), shade3D(tint, 1.12), edge)
    out += Face3(Vector(top, left, near), shade3D(tint, 0.82), edge)
    out += Face3(Vector(bottom, right, near), shade3D(tint, 0.62), edge)
    out += Face3(Vector(bottom, far, right), shade3D(tint, 0.76), edge)
    out += Face3(Vector(bottom, left, far), shade3D(tint, 0.68), edge)
    out += Face3(Vector(bottom, near, left), shade3D(tint, 0.55), edge)
  }

  /** A low-poly UV sphere: intentionally faceted for a lightweight game renderer. */
  private def addSphere3D(out: ArrayBuffer[Face3], x: Double, y: Double, z: Double,
                          radius: Double, tint: Color): Unit = {
    val bands = 4
    val slices = 7
    var lat = 0
    while (lat < bands) {
      val p0 = -math.Pi / 2.0 + math.Pi * lat / bands
      val p1 = -math.Pi / 2.0 + math.Pi * (lat + 1) / bands
      var lon = 0
      while (lon < slices) {
        val t0 = math.Pi * 2.0 * lon / slices + frameCount * 0.0008
        val t1 = math.Pi * 2.0 * (lon + 1) / slices + frameCount * 0.0008
        def point(phi: Double, theta: Double): V3 = V3(
          x + math.cos(phi) * math.cos(theta) * radius,
          y + math.cos(phi) * math.sin(theta) * radius,
          z + math.sin(phi) * radius
        )
        val factor = 0.78 + 0.25 * math.max(0.0, math.sin((lon.toDouble / slices) * math.Pi * 2.0 + 0.8))
        out += Face3(Vector(point(p0, t0), point(p0, t1), point(p1, t1), point(p1, t0)),
          shade3D(tint, factor), shade3D(tint, 0.42))
        lon += 1
      }
      lat += 1
    }
  }

  private def render3DFaces(g: Graphics2D, faces: ArrayBuffer[Face3], w: Int, h: Int): Unit = {
    val projected = ArrayBuffer[(Double, Face3, Vector[P3])]()
    faces.foreach { face =>
      val points = face.points.map(v => project3D(v, w, h))
      if (points.forall(_.isDefined)) {
        val valid = points.map(_.get)
        val averageDepth = valid.map(_.depth).sum / math.max(1, valid.length)
        projected += ((averageDepth, face, valid))
      }
    }
    // Painter's algorithm: far polygons first, near polygons last.
    projected.sortBy(item => -item._1).foreach { item =>
      val face = item._2
      val pts = item._3
      val xs = pts.map(_.x).toArray
      val ys = pts.map(_.y).toArray
      g.setColor(fog3D(face.fill, item._1))
      g.fillPolygon(xs, ys, pts.length)
      g.setColor(fog3D(face.edge, item._1))
      g.setStroke(new BasicStroke(1.0f))
      g.drawPolygon(xs, ys, pts.length)
    }
  }

  private def draw3DLine(g: Graphics2D, a: V3, b: V3, color: Color, width: Float, w: Int, h: Int): Unit = {
    val pa = project3D(a, w, h)
    val pb = project3D(b, w, h)
    if (pa.isDefined && pb.isDefined) {
      g.setColor(color)
      g.setStroke(new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
      g.drawLine(pa.get.x, pa.get.y, pb.get.x, pb.get.y)
    }
  }

  private def draw3DHorizontalRing(g: Graphics2D, x: Double, y: Double, z: Double,
                                   radius: Double, tint: Color, w: Int, h: Int): Unit = {
    var i = 0
    while (i < 44) {
      val a0 = i * math.Pi * 2.0 / 44.0
      val a1 = (i + 1) * math.Pi * 2.0 / 44.0
      draw3DLine(g, V3(x + math.cos(a0) * radius, y + math.sin(a0) * radius, z),
        V3(x + math.cos(a1) * radius, y + math.sin(a1) * radius, z), tint, 2.0f, w, h)
      i += 1
    }
  }

  private def draw3DVerticalRing(g: Graphics2D, x: Double, y: Double, z: Double,
                                 radius: Double, tint: Color, w: Int, h: Int): Unit = {
    val f = cameraForward
    val rx = -f._2
    val ry = f._1
    var i = 0
    while (i < 52) {
      val a0 = i * math.Pi * 2.0 / 52.0
      val a1 = (i + 1) * math.Pi * 2.0 / 52.0
      val p0 = V3(x + rx * math.cos(a0) * radius, y + ry * math.cos(a0) * radius, z + math.sin(a0) * radius)
      val p1 = V3(x + rx * math.cos(a1) * radius, y + ry * math.cos(a1) * radius, z + math.sin(a1) * radius)
      draw3DLine(g, p0, p1, tint, 4.0f, w, h)
      i += 1
    }
  }

  private def draw3DLabel(g: Graphics2D, text: String, x: Double, y: Double, z: Double,
                          color: Color, size: Int, w: Int, h: Int): Unit = {
    project3D(V3(x, y, z), w, h).foreach { p =>
      g.setFont(new Font("Dialog", Font.BOLD, size))
      g.setColor(new Color(4, 12, 28, 190))
      drawCentered(g, text, p.x + 1, p.y + 2)
      g.setColor(color)
      drawCentered(g, text, p.x, p.y)
    }
  }

  private def draw3DBackdrop(g: Graphics2D, w: Int, h: Int): Unit = {
    val upper = colorFromHue(currentRealm.hue, 0.76f, 0.25f)
    val lower = new Color(8, 23, 43)
    g.setPaint(new GradientPaint(0f, 0f, upper, 0f, h.toFloat, lower))
    g.fillRect(0, 0, w, h)
    stars.foreach { star =>
      val x = ((star.x / WORLD_W) * w).toInt
      val y = ((star.y / WORLD_H) * (h * 0.43)).toInt
      val alpha = 90 + math.abs((math.sin(frameCount * 0.055 + star.phase) * 145).toInt)
      g.setColor(new Color(226, 244, 255, alpha))
      g.fillOval(x, y, star.size + 1, star.size + 1)
    }
    val mx = (w * 0.78).toInt
    val my = (h * 0.16).toInt
    drawGlow(g, mx, my, 118, C_GOLD, 0.13f)
    g.setColor(new Color(255, 230, 185))
    g.fillOval(mx - 31, my - 31, 62, 62)
    g.setColor(new Color(255, 255, 255, 110))
    g.drawOval(mx - 44, my - 44, 88, 88)
  }

  private def draw3DScene(g: Graphics2D, w: Int, h: Int): Unit = {
    draw3DBackdrop(g, w, h)
    val groundFaces = ArrayBuffer[Face3]()
    // Floating world foundation plus individually projected floor tiles. Small tiles
    // keep the camera's near-plane clipping from removing the entire ground polygon.
    addBox3D(groundFaces, WORLD_W * 0.5, WORLD_H * 0.5, -42.0, WORLD_W, WORLD_H, 42.0, new Color(21, 91, 88))
    var tileX = 0.0
    while (tileX < WORLD_W) {
      var tileY = 0.0
      while (tileY < WORLD_H) {
        val tileTint = if (((tileX / 240.0).toInt + (tileY / 220.0).toInt) % 2 == 0) new Color(23, 91, 82) else new Color(18, 78, 80)
        groundFaces += Face3(Vector(V3(tileX, tileY, 0.0), V3(tileX + 240.0, tileY, 0.0),
          V3(tileX + 240.0, tileY + 220.0, 0.0), V3(tileX, tileY + 220.0, 0.0)),
          tileTint, new Color(84, 177, 154, 80))
        tileY += 220.0
      }
      tileX += 240.0
    }

    // Soft low-poly contact shadows make the raised geometry feel grounded.
    var shadowIndex = 0
    while (shadowIndex < 12) {
      val sx = 145.0 + (shadowIndex % 4) * 510.0
      val sy = 170.0 + (shadowIndex / 4) * 420.0 + (shadowIndex % 2) * 65.0
      addDisc3D(groundFaces, sx, sy, 1.8, 112.0, 74.0, new Color(3, 13, 25, 55))
      shadowIndex += 1
    }
    render3DFaces(g, groundFaces, w, h)

    // Perspective floor grid, drawn above the ground tiles but below all props.
    var gx = 0.0
    while (gx <= WORLD_W) {
      draw3DLine(g, V3(gx, 0.0, 1.2), V3(gx, WORLD_H, 1.2), new Color(98, 223, 194, 72), 1.0f, w, h)
      gx += 150.0
    }
    var gy = 0.0
    while (gy <= WORLD_H) {
      draw3DLine(g, V3(0.0, gy, 1.2), V3(WORLD_W, gy, 1.2), new Color(98, 223, 194, 72), 1.0f, w, h)
      gy += 150.0
    }

    val faces = ArrayBuffer[Face3]()

    // Raised stone platforms and layered walkways.
    var platform = 0
    while (platform < 12) {
      val px = 145.0 + (platform % 4) * 510.0
      val py = 170.0 + (platform / 4) * 420.0 + (platform % 2) * 65.0
      addBox3D(faces, px, py, 0.0, 190.0, 126.0, 38.0, new Color(35, 113, 104))
      addBox3D(faces, px, py, 38.0, 154.0, 94.0, 8.0, new Color(77, 178, 137))
      platform += 1
    }

    // Sanctus engine: layered plinths, eight pillars, animated rings and a crystal core.
    addBox3D(faces, 960.0, 600.0, 0.0, 300.0, 245.0, 38.0, new Color(36, 69, 105))
    addBox3D(faces, 960.0, 600.0, 38.0, 240.0, 185.0, 24.0, new Color(57, 115, 138))
    var pillar = 0
    while (pillar < 8) {
      val angle = pillar * math.Pi * 2.0 / 8.0
      val px = 960.0 + math.cos(angle) * 116.0
      val py = 600.0 + math.sin(angle) * 91.0
      addBox3D(faces, px, py, 62.0, 23.0, 23.0, 152.0, new Color(62, 109, 143))
      addBox3D(faces, px, py, 214.0, 33.0, 33.0, 15.0, new Color(139, 215, 194))
      addSphere3D(faces, px, py, 239.0, 10.0, if (pillar % 2 == 0) C_GOLD else C_MINT)
      pillar += 1
    }
    addSphere3D(faces, 960.0, 600.0, 154.0, 48.0, colorFromHue(currentRealm.hue, 0.58f, 1.0f))
    addOcta3D(faces, 960.0, 600.0, 185.0 + math.sin(frameCount * 0.055) * 8.0, 33.0, 76.0, C_GOLD)

    // Low-poly trees and glowing growths, spaced through the whole realm.
    var bloomIndex = 0
    blooms.foreach { bloom =>
      if (bloomIndex % 18 == 0) {
        addBox3D(faces, bloom.x, bloom.y, 0.0, 17.0, 17.0, 62.0, new Color(87, 64, 53))
        addSphere3D(faces, bloom.x, bloom.y, 93.0, 39.0, colorFromHue(bloom.hue, 0.64f, 0.76f))
        addSphere3D(faces, bloom.x - 19.0, bloom.y + 7.0, 111.0, 22.0, colorFromHue(bloom.hue + 14, 0.56f, 0.92f))
      }
      bloomIndex += 1
    }

    // Collectible sigils and floating power cores.
    relics.foreach { relic =>
      if (!relic.collected) {
        val bob = math.sin(frameCount * 0.075 + relic.x * 0.01) * 9.0
        addBox3D(faces, relic.x, relic.y, 0.0, 40.0, 40.0, 10.0, new Color(40, 89, 102))
        addOcta3D(faces, relic.x, relic.y, 42.0 + bob, 17.0, 31.0, relic.tint)
      }
    }
    gemDrops.foreach { drop =>
      if (!drop.collected) {
        val gem = POWER_GEMS(drop.gemId)
        val bob = math.sin(frameCount * 0.10 + drop.gemId) * 12.0
        addOcta3D(faces, drop.x, drop.y, 52.0 + bob, 22.0, 39.0, gem.tint)
        addSphere3D(faces, drop.x, drop.y, 53.0 + bob, 10.0, Color.WHITE)
      }
    }

    // Sentries are fully built from 3D body, head, armour, shoulders and feet meshes.
    sentries.foreach { enemy =>
      if (!enemy.defeated) {
        addDisc3D(faces, enemy.x, enemy.y, 2.0, 43.0, 34.0, new Color(2, 9, 19, 105))
        val body = if (enemy.disabled > 0.0) C_BLUE else if (enemy.hitFlash > 0.0) C_WHITE else enemy.tint
        val scale = 1.0 + enemy.rank * 0.055
        addBox3D(faces, enemy.x - 15.0, enemy.y, 0.0, 25.0 * scale, 31.0 * scale, 38.0 * scale, new Color(33, 37, 63))
        addBox3D(faces, enemy.x + 15.0, enemy.y, 0.0, 25.0 * scale, 31.0 * scale, 38.0 * scale, new Color(33, 37, 63))
        addBox3D(faces, enemy.x, enemy.y, 34.0 * scale, 78.0 * scale, 54.0 * scale, 72.0 * scale, shade3D(body, 0.79))
        addBox3D(faces, enemy.x, enemy.y - 3.0, 49.0 * scale, 54.0 * scale, 59.0 * scale, 54.0 * scale, body)
        addSphere3D(faces, enemy.x, enemy.y - 5.0, 116.0 * scale, 31.0 * scale, shade3D(body, 1.12))
        addOcta3D(faces, enemy.x - 23.0 * scale, enemy.y - 4.0, 144.0 * scale, 10.0 * scale, 22.0 * scale, C_PINK)
        addOcta3D(faces, enemy.x + 23.0 * scale, enemy.y - 4.0, 144.0 * scale, 10.0 * scale, 22.0 * scale, C_PINK)
        addSphere3D(faces, enemy.x - 11.0 * scale, enemy.y - 31.0, 120.0 * scale, 6.0 * scale, C_GOLD)
        addSphere3D(faces, enemy.x + 11.0 * scale, enemy.y - 31.0, 120.0 * scale, 6.0 * scale, C_GOLD)
      }
    }

    // Local player: boots, legs, torso, shoulders, head and the five-slot gauntlet.
    val playerBlink = invulnerability > 0.0 && (frameCount / 5) % 2 == 0
    if (!playerBlink) {
      addDisc3D(faces, playerX, playerY, 2.2, 34.0, 29.0, new Color(2, 9, 19, 125))
      val bob = math.sin(frameCount * 0.18) * 3.0
      addBox3D(faces, playerX - 15.0, playerY, 0.0, 22.0, 27.0, 48.0 + bob, new Color(31, 48, 82))
      addBox3D(faces, playerX + 15.0, playerY, 0.0, 22.0, 27.0, 48.0 - bob, new Color(31, 48, 82))
      addBox3D(faces, playerX, playerY, 45.0, 57.0, 38.0, 73.0 + bob, new Color(36, 91, 134))
      addBox3D(faces, playerX, playerY - 2.0, 58.0, 43.0, 43.0, 49.0 + bob, new Color(90, 211, 190))
      addSphere3D(faces, playerX, playerY, 143.0 + bob, 24.0, new Color(238, 202, 171))
      addSphere3D(faces, playerX, playerY + 2.0, 158.0 + bob, 25.0, new Color(42, 32, 59))
      addBox3D(faces, playerX - 39.0, playerY, 57.0, 19.0, 25.0, 55.0, new Color(43, 105, 143))
      addBox3D(faces, playerX + 39.0, playerY, 57.0, 19.0, 25.0, 55.0, new Color(43, 105, 143))
      val f = cameraForward
      val gloveX = playerX - f._2 * 43.0 + f._1 * 10.0
      val gloveY = playerY + f._1 * 43.0 + f._2 * 10.0
      addBox3D(faces, gloveX, gloveY, 71.0, 27.0, 29.0, 30.0, new Color(167, 122, 60))
      currentGem.foreach { gem =>
        addOcta3D(faces, gloveX, gloveY - 2.0, 104.0, 12.0, 19.0, gem.tint)
      }
    }

    // Remote players are also rendered as 3D avatars in the shared arena.
    networkPlayers.foreach { peer =>
      if (peer.id != localNetworkId) {
        addBox3D(faces, peer.x - 13.0, peer.y, 0.0, 22.0, 25.0, 43.0, new Color(30, 50, 95))
        addBox3D(faces, peer.x + 13.0, peer.y, 0.0, 22.0, 25.0, 43.0, new Color(30, 50, 95))
        addBox3D(faces, peer.x, peer.y, 42.0, 52.0, 37.0, 69.0, new Color(45, 117, 171))
        addSphere3D(faces, peer.x, peer.y, 132.0, 23.0, new Color(225, 194, 166))
      }
    }

    // Exit gate and animated energy effects.
    addBox3D(faces, PORTAL_X, PORTAL_Y + 42.0, 0.0, 155.0, 65.0, 22.0, new Color(47, 71, 114))
    render3DFaces(g, faces, w, h)

    draw3DVerticalRing(g, 960.0, 600.0, 138.0, 116.0, C_MINT, w, h)
    draw3DHorizontalRing(g, 960.0, 600.0, 4.0, 160.0, new Color(100, 229, 218, 190), w, h)
    draw3DVerticalRing(g, PORTAL_X, PORTAL_Y, 102.0, 86.0, if (relicCount == relics.length && aliveEnemyCount == 0) C_GOLD else C_VIOLET, w, h)
    draw3DHorizontalRing(g, playerX, playerY, 3.0, 34.0, new Color(170, 255, 220, 135), w, h)

    relics.foreach { relic => if (!relic.collected) draw3DLabel(g, relic.title, relic.x, relic.y, 86.0, relic.tint, 9, w, h) }
    gemDrops.foreach { drop => if (!drop.collected) draw3DLabel(g, POWER_GEMS(drop.gemId).name, drop.x, drop.y, 105.0, POWER_GEMS(drop.gemId).tint, 10, w, h) }
    sentries.foreach { enemy =>
      if (!enemy.defeated) {
        draw3DLabel(g, if (enemy.disabled > 0.0) "FROZEN" else "SENTRY " + enemy.rank,
          enemy.x, enemy.y, 177.0, C_WHITE, 10, w, h)
        val barLeft = project3D(V3(enemy.x - 32.0, enemy.y, 166.0), w, h)
        val barRight = project3D(V3(enemy.x + 32.0, enemy.y, 166.0), w, h)
        if (barLeft.isDefined && barRight.isDefined) {
          val left = barLeft.get
          val right = barRight.get
          val barW = math.max(1, right.x - left.x)
          g.setColor(new Color(23, 31, 49, 220)); g.fillRoundRect(left.x, left.y, barW, 6, 4, 4)
          g.setColor(C_PINK); g.fillRoundRect(left.x, left.y, (barW * enemy.health.toDouble / enemy.maxHealth).toInt, 6, 4, 4)
        }
      }
    }
    draw3DLabel(g, playerName, playerX, playerY, 194.0, C_GOLD, 11, w, h)
    draw3DLabel(g, "SANCTUS ENGINE", 960.0, 600.0, 285.0, C_MINT, 12, w, h)
    draw3DLabel(g, if (relicCount == relics.length && aliveEnemyCount == 0) "GATE OPEN" else "GATE SEALED",
      PORTAL_X, PORTAL_Y, 206.0, C_GOLD, 11, w, h)

    // Sparks float above the floor in world space.
    sparks.foreach { spark =>
      val height = 10.0 + (1.0 - clamp(spark.life / 2.0, 0.0, 1.0)) * 38.0
      project3D(V3(spark.x, spark.y, height), w, h).foreach { p =>
        val alpha = clamp(spark.life / 3.0, 0.0, 0.82).toFloat
        val old = g.getComposite
        g.setComposite(AlphaComposite.SrcOver.derive(alpha))
        drawGlow(g, p.x, p.y, math.max(3, spark.size.toInt + 2), spark.tint, 0.24f)
        g.setColor(spark.tint); g.fillOval(p.x - 2, p.y - 2, 4, 4)
        g.setComposite(old)
      }
    }

    if (attackFlash > 0.0) {
      val f = cameraForward
      draw3DVerticalRing(g, playerX + f._1 * 75.0, playerY + f._2 * 75.0, 72.0,
        45.0 + (1.0 - attackFlash / 0.20) * 38.0, C_GOLD, w, h)
    }
    if (pulseFlash > 0.0) {
      draw3DHorizontalRing(g, playerX, playerY, 8.0,
        75.0 + (0.55 - pulseFlash) * 260.0, new Color(90, 202, 255, 190), w, h)
    }
    g.setFont(new Font("Dialog", Font.BOLD, 10))
    g.setColor(new Color(240, 250, 255, 210))
    g.drawString("REAL-TIME PERSPECTIVE VIEW  ?  CAMERA: " + CAMERA_LABELS(cameraDistanceIndex) + "  ?  V: CAMERA DISTANCE", 18, h - 62)
  }

  // ---------------------------------------------------------------------------
  // DRAWING UTILITIES
  // ---------------------------------------------------------------------------
  private def drawGlow(g: Graphics2D, x: Int, y: Int, radius: Int, color: Color, alpha: Float): Unit = {
    val old = g.getComposite
    g.setComposite(AlphaComposite.SrcOver.derive(alpha))
    g.setColor(color)
    g.fillOval(x - radius, y - radius, radius * 2, radius * 2)
    g.setComposite(old)
  }

  private def drawCentered(g: Graphics2D, text: String, x: Int, y: Int): Unit = {
    g.drawString(text, x - g.getFontMetrics.stringWidth(text) / 2, y)
  }

  private def drawBackdrop(g: Graphics2D, w: Int, h: Int): Unit = {
    val hue = currentRealm.hue
    val top = colorFromHue(hue, 0.73f, 0.22f)
    val bottom = new Color(8, 34, 49)
    g.setPaint(new GradientPaint(0f, 0f, top, 0f, h.toFloat, bottom))
    g.fillRect(0, 0, w, h)

    stars.foreach { s =>
      val x = worldX(s.x)
      val y = worldY(s.y)
      if (x >= 0 && y >= 0 && x < w && y < h) {
        val alpha = 95 + math.abs((math.sin(frameCount * 0.045 + s.phase) * 145).toInt)
        g.setColor(new Color(230, 247, 255, alpha))
        g.fillOval(x, y, s.size, s.size)
      }
    }

    val moonX = worldX(1470.0)
    val moonY = worldY(155.0)
    drawGlow(g, moonX, moonY, 115, C_GOLD, 0.12f)
    g.setColor(new Color(255, 229, 177))
    g.fillOval(moonX - 34, moonY - 34, 68, 68)
    g.setColor(new Color(255, 255, 255, 110))
    g.drawOval(moonX - 45, moonY - 45, 90, 90)
  }

  private def drawTerrain(g: Graphics2D, w: Int, h: Int): Unit = {
    val top = (h * 0.31).toInt
    g.setPaint(new GradientPaint(0f, top.toFloat, new Color(30, 104, 93), 0f, h.toFloat, new Color(5, 34, 46)))
    g.fillRect(0, top, w, h - top)
    val old = g.getComposite
    g.setComposite(AlphaComposite.SrcOver.derive(0.075f))
    g.setColor(C_MINT)
    var x = 0
    while (x < w) { g.drawLine(x, top, x, h); x += 78 }
    var y = top
    while (y < h) { g.drawLine(0, y, w, y); y += 78 }
    g.setComposite(old)

    // Floating garden shelves add depth without external images.
    var i = 0
    while (i < 12) {
      val wx = 130.0 + i * 157.0
      val wy = 215.0 + (i % 3) * 295.0
      val sx = worldX(wx)
      val sy = worldY(wy)
      if (sx > -170 && sx < w + 170 && sy > -100 && sy < h + 100) {
        g.setColor(new Color(6, 36, 47, 170))
        g.fillOval(sx - 65, sy - 8, 130, 42)
        g.setColor(new Color(71, 170, 134, 150))
        g.fillOval(sx - 62, sy - 15, 124, 29)
        g.setColor(new Color(168, 246, 213, 110))
        g.drawArc(sx - 42, sy - 17, 84, 20, 15, 145)
      }
      i += 1
    }
  }

  private def drawLandmark(g: Graphics2D): Unit = {
    val cx = worldX(960.0)
    val cy = worldY(600.0)
    val tone = colorFromHue(currentRealm.hue, 0.7f, 0.95f)
    drawGlow(g, cx, cy - 55, 190, tone, 0.10f)

    // New central landmark: a multi-ring lumen engine.
    g.setColor(new Color(13, 25, 49, 215))
    g.fillOval(cx - 120, cy - 93, 240, 186)
    g.setStroke(new BasicStroke(5.0f))
    g.setColor(tone)
    g.drawOval(cx - 105, cy - 115, 210, 210)
    g.setStroke(new BasicStroke(2.0f))
    g.setColor(new Color(232, 252, 255, 190))
    g.drawOval(cx - 82, cy - 91, 164, 164)

    var i = 0
    while (i < 9) {
      val a = frameCount * 0.012 + i * math.Pi * 2.0 / 9.0
      val px = cx + (math.cos(a) * 92).toInt
      val py = cy - 10 + (math.sin(a) * 92).toInt
      g.setColor(if (i % 2 == 0) C_GOLD else C_MINT)
      g.fillOval(px - 7, py - 7, 14, 14)
      i += 1
    }

    g.setColor(new Color(35, 77, 97))
    g.fillRoundRect(cx - 22, cy - 145, 44, 100, 15, 15)
    g.setColor(new Color(190, 240, 233))
    g.fillPolygon(Array(cx, cx + 18, cx, cx - 18), Array(cy - 182, cy - 139, cy - 95, cy - 139), 4)
    g.setFont(new Font("Dialog", Font.BOLD, 12))
    g.setColor(C_WHITE)
    drawCentered(g, "LUMEN ENGINE", cx, cy + 132)
  }

  private def drawBlooms(g: Graphics2D): Unit = {
    blooms.foreach { b =>
      val x = worldX(b.x)
      val y = worldY(b.y)
      if (x > -20 && y > -20 && x < 1940 && y < 1340) {
        val r = math.max(2, (b.size * (0.86 + 0.14 * math.sin(frameCount * 0.05 + b.phase))).toInt)
        g.setColor(new Color(40, 128, 88))
        g.drawLine(x, y + 2, x, y + r * 3)
        g.setColor(colorFromHue(b.hue))
        var k = 0
        while (k < 5) {
          val a = k * math.Pi * 2.0 / 5.0
          g.fillOval(x + (math.cos(a) * r * 0.72).toInt - r / 2, y + (math.sin(a) * r * 0.72).toInt - r / 2, r, r)
          k += 1
        }
        g.setColor(C_GOLD)
        g.fillOval(x - 2, y - 2, 4, 4)
      }
    }
  }

  private def drawRelic(g: Graphics2D, r: Relic): Unit = {
    if (r.collected) return
    val x = worldX(r.x)
    val y = worldY(r.y)
    if (x < -75 || y < -75 || x > 1975 || y > 1395) return
    val pulse = 13 + (3 * math.sin(frameCount * 0.075 + r.x)).toInt
    drawGlow(g, x, y, pulse * 3, r.tint, 0.13f)
    g.setColor(r.tint)
    g.fillPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
    g.setColor(C_WHITE)
    g.drawPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
    g.setFont(new Font("Dialog", Font.BOLD, 9))
    drawCentered(g, r.title, x, y - pulse - 14)
    g.setColor(C_MINT)
    drawCentered(g, "E: TAKE", x, y + pulse + 17)
  }

  private def drawSentry(g: Graphics2D, s: Sentry): Unit = {
    if (s.defeated) return
    val x = worldX(s.x)
    val y = worldY(s.y)
    if (x < -90 || y < -90 || x > 1980 || y > 1400) return
    val body = if (s.disabled > 0.0) C_BLUE else if (s.hitFlash > 0.0) C_WHITE else s.tint
    drawGlow(g, x, y, 52, body, if (s.disabled > 0.0) 0.10f else 0.15f)
    g.setColor(new Color(23, 30, 59))
    g.fillOval(x - 30, y - 27, 60, 54)
    g.setColor(body)
    g.fillOval(x - 25, y - 24, 50, 47)
    g.setColor(new Color(255, 250, 228))
    g.fillOval(x - 12, y - 6, 8, 9)
    g.fillOval(x + 5, y - 6, 8, 9)
    g.setColor(new Color(37, 42, 63))
    g.fillOval(x - 10, y - 3, 3, 4)
    g.fillOval(x + 8, y - 3, 3, 4)
    g.setColor(new Color(38, 48, 69))
    g.fillRoundRect(x - 27, y - 42, 54, 6, 4, 4)
    g.setColor(C_PINK)
    g.fillRoundRect(x - 27, y - 42, (54.0 * s.health / math.max(1, s.maxHealth)).toInt, 6, 4, 4)
    g.setFont(new Font("Dialog", Font.BOLD, 9))
    g.setColor(C_WHITE)
    drawCentered(g, if (s.disabled > 0.0) "FROZEN" else "ENEMY T" + s.rank, x, y - 50)
  }

  private def drawGemDrop(g: Graphics2D, drop: GemDrop): Unit = {
    if (drop.collected) return
    val gem = POWER_GEMS(drop.gemId)
    val x = worldX(drop.x)
    val y = worldY(drop.y)
    val pulse = 12 + (3 * math.sin(frameCount * 0.09 + drop.gemId)).toInt
    drawGlow(g, x, y, pulse * 3, gem.tint, 0.18f)
    g.setColor(gem.tint)
    g.fillPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
    g.setColor(C_WHITE)
    g.drawPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
    g.setFont(new Font("Dialog", Font.BOLD, 9))
    drawCentered(g, gem.name, x, y - pulse - 15)
    g.setColor(C_GOLD)
    drawCentered(g, "E: COLLECT", x, y + pulse + 17)
  }

  private def drawExitArch(g: Graphics2D): Unit = {
    val x = worldX(PORTAL_X)
    val y = worldY(PORTAL_Y)
    val ready = relicCount == relics.length && aliveEnemyCount == 0
    val tint = if (ready) C_GOLD else C_VIOLET
    val pulse = 1.0 + math.sin(frameCount * 0.045) * 0.055
    val width = (118 * pulse).toInt
    val height = (154 * pulse).toInt
    drawGlow(g, x, y, 116, tint, 0.13f)
    g.setColor(new Color(7, 17, 39, 235))
    g.fillOval(x - width / 2, y - height / 2, width, height)
    g.setStroke(new BasicStroke(6.0f))
    g.setColor(tint)
    g.drawOval(x - width / 2, y - height / 2, width, height)
    g.setStroke(new BasicStroke(2.0f))
    g.setColor(new Color(193, 245, 255, 175))
    g.drawOval(x - width / 2 + 13, y - height / 2 + 14, width - 26, height - 28)
    var i = 0
    while (i < 8) {
      val a = frameCount * 0.022 + i * math.Pi * 2.0 / 8.0
      val px = x + (math.cos(a) * 48).toInt
      val py = y + (math.sin(a) * 61).toInt
      g.setColor(if (i % 2 == 0) C_MINT else C_PINK)
      g.fillOval(px - 4, py - 4, 8, 8)
      i += 1
    }
    g.setFont(new Font("Dialog", Font.BOLD, 10))
    g.setColor(tint)
    drawCentered(g, if (ready) "EXIT OPEN" else "EXIT SEALED", x, y + height / 2 + 20)
  }

  private def drawPlayer(g: Graphics2D): Unit = {
    val x = worldX(playerX)
    val y = worldY(playerY)
    val bob = math.sin(frameCount * 0.18) * 2.0
    if (invulnerability > 0.0 && (frameCount / 5) % 2 == 0) return
    drawGlow(g, x, y, 44, C_MINT, 0.13f)
    g.setColor(new Color(0, 0, 0, 90))
    g.fillOval(x - 19, y + 15, 38, 11)
    g.setColor(new Color(34, 55, 91))
    g.fillRoundRect(x - 14, y + bob.toInt, 11, 21, 6, 6)
    g.fillRoundRect(x + 3, y - bob.toInt, 11, 21, 6, 6)
    g.setColor(C_GOLD)
    g.fillRoundRect(x - 15, y + 16, 13, 6, 3, 3)
    g.fillRoundRect(x + 2, y + 16, 13, 6, 3, 3)
    g.setColor(new Color(37, 91, 128))
    g.fillRoundRect(x - 19, y - 27 + bob.toInt, 38, 39, 12, 12)
    g.setColor(new Color(94, 218, 194))
    g.fillRoundRect(x - 14, y - 22 + bob.toInt, 28, 28, 9, 9)
    g.setColor(new Color(241, 207, 177))
    g.fillOval(x - 12, y - 50 + bob.toInt, 24, 25)
    g.setColor(new Color(37, 30, 54))
    g.fillArc(x - 13, y - 57 + bob.toInt, 26, 25, 0, 180)
    g.setColor(C_WHITE)
    g.fillOval(x - 5, y - 40 + bob.toInt, 3, 3)
    g.fillOval(x + 3, y - 40 + bob.toInt, 3, 3)
    g.setFont(new Font("Dialog", Font.BOLD, 10))
    g.setColor(C_WHITE)
    drawCentered(g, playerName, x, y - 66)
  }

  private def drawRemotePlayers(g: Graphics2D): Unit = {
    networkPlayers.foreach { peer =>
      if (peer.id != localNetworkId) {
        val x = worldX(peer.x)
        val y = worldY(peer.y)
        if (x > -40 && y > -70 && x < 1260 && y < 820) {
          drawGlow(g, x, y, 36, C_BLUE, 0.12f)
          g.setColor(new Color(40, 91, 150))
          g.fillRoundRect(x - 14, y - 25, 28, 40, 10, 10)
          g.setColor(C_BLUE)
          g.fillRoundRect(x - 10, y - 21, 20, 27, 8, 8)
          g.setColor(C_WHITE)
          g.fillOval(x - 9, y - 47, 18, 19)
          g.setFont(new Font("Dialog", Font.BOLD, 10))
          g.setColor(C_GOLD)
          drawCentered(g, peer.name, x, y - 56)
        }
      }
    }
  }

  private def drawGauntlet(g: Graphics2D): Unit = {
    val x = worldX(playerX) + 17
    val y = worldY(playerY) + 2
    g.setColor(new Color(179, 136, 69))
    g.fillRoundRect(x - 2, y - 3, 11, 17, 4, 4)
    currentGem.foreach { gem =>
      drawGlow(g, x + 4, y + 2, 13, gem.tint, 0.24f)
      g.setColor(gem.tint)
      g.fillOval(x + 1, y - 1, 7, 7)
      g.setColor(C_WHITE)
      g.drawOval(x + 1, y - 1, 7, 7)
    }
  }

  private def drawSparks(g: Graphics2D): Unit = {
    sparks.foreach { p =>
      val x = worldX(p.x)
      val y = worldY(p.y)
      if (x >= -10 && y >= -10 && x < 1930 && y < 1350) {
        val alpha = clamp(p.life / 4.0, 0.0, 0.86).toFloat
        val old = g.getComposite
        g.setComposite(AlphaComposite.SrcOver.derive(alpha))
        g.setColor(p.tint)
        val size = math.max(2, p.size.toInt)
        g.fillOval(x - size / 2, y - size / 2, size, size)
        g.setColor(C_WHITE)
        g.fillOval(x - 1, y - 1, 2, 2)
        g.setComposite(old)
      }
    }
  }

  private def drawMiniMap(g: Graphics2D, w: Int): Unit = {
    val x = w - 193
    val y = 106
    val mw = 172
    val mh = 118
    g.setColor(new Color(4, 12, 28, 225))
    g.fillRoundRect(x, y, mw, mh, 14, 14)
    g.setColor(new Color(98, 211, 209, 175))
    g.drawRoundRect(x, y, mw, mh, 14, 14)
    def mx(v: Double): Int = x + 8 + (v / WORLD_W * (mw - 16)).toInt
    def my(v: Double): Int = y + 8 + (v / WORLD_H * (mh - 16)).toInt
    relics.filterNot(_.collected).foreach { r =>
      g.setColor(r.tint)
      g.fillOval(mx(r.x) - 3, my(r.y) - 3, 6, 6)
    }
    g.setColor(C_GOLD)
    g.fillOval(mx(PORTAL_X) - 4, my(PORTAL_Y) - 4, 8, 8)
    g.setColor(C_WHITE)
    g.fillOval(mx(playerX) - 3, my(playerY) - 3, 6, 6)
    g.setFont(new Font("Dialog", Font.BOLD, 9))
    g.drawString("FIELD SCAN", x + 9, y + 13)
  }

  private def drawHUD(g: Graphics2D, w: Int, h: Int): Unit = {
    g.setColor(new Color(4, 13, 31, 225))
    g.fillRoundRect(14, 13, 505, 119, 17, 17)
    g.setColor(new Color(89, 208, 199, 170))
    g.drawRoundRect(14, 13, 505, 119, 17, 17)
    g.setFont(new Font("Dialog", Font.BOLD, 19))
    g.setColor(C_MINT)
    g.drawString("SANCTUS  /  SPRING SEASON 04", 29, 41)
    g.setFont(new Font("Dialog", Font.BOLD, 11))
    g.setColor(C_WHITE)
    g.drawString("REALM " + activeRealm + "  ?  " + currentRealm.name, 29, 63)
    g.setColor(C_GOLD)
    g.setFont(new Font("Dialog", Font.PLAIN, 10))
    g.drawString("SIGILS  " + relicCount + " / " + relics.length + "    ENEMIES  " + aliveEnemyCount + "    KOs  " + enemyKills, 29, 84)
    g.setColor(new Color(35, 57, 77))
    g.fillRoundRect(29, 96, 215, 9, 8, 8)
    g.setColor(C_MINT)
    g.fillRoundRect(29, 96, (215.0 * relicCount / math.max(1, relics.length)).toInt, 9, 8, 8)
    g.setColor(C_PINK)
    g.drawString("CHARGE  " + ("? " * charge), 276, 103)
    g.setColor(C_SOFT)
    g.setFont(new Font("Dialog", Font.PLAIN, 10))
    g.drawString(selectedDifficulty + "  /  " + selectedMode + "  /  " + multiplayerMode, 276, 119)
    g.setColor(new Color(35, 57, 77))
    g.fillRoundRect(29, 111, 215, 7, 7, 7)
    g.setColor(C_PINK)
    g.fillRoundRect(29, 111, (215.0 * playerHealth / 100.0).toInt, 7, 7, 7)
    g.setFont(new Font("Dialog", Font.BOLD, 8))
    g.setColor(C_WHITE)
    g.drawString("HP " + playerHealth + "/100", 250, 117)

    g.setColor(new Color(4, 13, 31, 225))
    g.fillRoundRect(w - 178, 16, 158, 67, 14, 14)
    g.setFont(new Font("Monospaced", Font.BOLD, 22))
    g.setColor(if (secondsLeft < 45.0) C_PINK else C_GOLD)
    val mm = (secondsLeft.toInt / 60) % 100
    val ss = secondsLeft.toInt % 60
    g.drawString(if (selectedMode == "FREE ROAM") "OPEN" else "%02d:%02d".format(mm, ss), w - 157, 48)
    g.setFont(new Font("Dialog", Font.PLAIN, 9))
    g.setColor(C_WHITE)
    g.drawString("TIME WINDOW", w - 155, 68)

    g.setColor(new Color(4, 13, 31, 225))
    g.fillRoundRect(15, h - 49, w - 30, 35, 12, 12)
    g.setFont(new Font("Dialog", Font.BOLD, 10))
    g.setColor(C_MINT)
    g.drawString("WASD / ARROWS  MOVE", 25, h - 27)
    g.setColor(C_GOLD)
    g.drawString("E COLLECT", 185, h - 27)
    g.setColor(C_PINK)
    g.drawString("F FIGHT", 270, h - 27)
    g.setColor(C_BLUE)
    g.drawString("R GEM POWER", 345, h - 27)
    g.setColor(C_MINT)
    g.drawString("1-5 EQUIP  ?  C COLLECTION", 455, h - 27)
    g.setColor(C_WHITE)
    g.drawString("SHIFT DASH  ?  P PAUSE  ?  T RETRY  ?  M REALMS  ?  O ADMIN  ?  V CAMERA", 690, h - 27)

    g.setColor(new Color(4, 13, 31, 220))
    g.fillRoundRect(15, 141, 355, 49, 12, 12)
    g.setFont(new Font("Dialog", Font.BOLD, 10))
    g.setColor(C_GOLD)
    g.drawString("ASTRAL GAUNTLET", 27, 159)
    g.setColor(C_WHITE)
    g.drawString("GEMS " + collectedGems.size + "/5", 27, 178)
    currentGem match {
      case Some(gem) =>
        g.setColor(gem.tint)
        g.drawString(gem.name + "  ?  " + gem.powerName, 112, 159)
        g.setColor(C_SOFT)
        g.drawString(if (powerCooldown <= 0.0) "R: READY" else "R: " + math.ceil(powerCooldown).toInt + "s", 112, 178)
      case None =>
        g.setColor(C_SOFT)
        g.drawString("NO GEM EQUIPPED  ?  COLLECT RUBY FIRST", 112, 168)
    }
    if (pulseFlash > 0.0) {
      val old = g.getComposite
      g.setComposite(AlphaComposite.SrcOver.derive((pulseFlash / 0.55 * 0.20).toFloat))
      g.setColor(C_BLUE)
      g.fillRect(0, 0, w, h)
      g.setComposite(old)
    }

    if (toastTime > 0.0) {
      val width = math.min(w - 40, math.max(330, g.getFontMetrics.stringWidth(toast) + 38))
      val tx = (w - width) / 2
      g.setColor(new Color(4, 15, 34, 235))
      g.fillRoundRect(tx, h - 96, width, 32, 14, 14)
      g.setColor(C_MINT)
      g.drawRoundRect(tx, h - 96, width, 32, 14, 14)
      g.setFont(new Font("Dialog", Font.BOLD, 11))
      g.setColor(C_WHITE)
      drawCentered(g, toast, w / 2, h - 76)
    }

    if (paused || cleared || failed) drawOverlay(g, w, h)
  }

  private def drawOverlay(g: Graphics2D, w: Int, h: Int): Unit = {
    g.setColor(new Color(3, 8, 23, 215))
    g.fillRect(0, 0, w, h)
    val title = if (cleared) "REALM STABILIZED" else if (failed) "SIGNAL LOST" else "FIELD PAUSED"
    val subtitle = if (cleared) "The exit arch has accepted your sigils." else if (failed) "Restore your charge and make another attempt." else "Your expedition is safely paused."
    g.setFont(new Font("Dialog", Font.BOLD, 37))
    g.setColor(if (cleared) C_GOLD else C_WHITE)
    drawCentered(g, title, w / 2, h / 2 - 35)
    g.setFont(new Font("Dialog", Font.PLAIN, 15))
    g.setColor(C_SOFT)
    drawCentered(g, subtitle, w / 2, h / 2 + 2)
    g.setFont(new Font("Dialog", Font.BOLD, 12))
    g.setColor(C_MINT)
    val instruction = if (cleared && activeRealm < realms.length) "ENTER  OPEN NEXT REALM     ?     M  RETURN TO REALMS" else "T  RETRY     ?     M  RETURN TO REALMS     ?     P  RESUME"
    drawCentered(g, instruction, w / 2, h / 2 + 43)
  }

  // ---------------------------------------------------------------------------
  // POWER GEM COLLECTION
  // ---------------------------------------------------------------------------
  private def buildCollectionScreen(): JPanel = {
    val p = verticalPanel()
    addCentered(p, makeTitle("ASTRAL GAUNTLET COLLECTION", 28, C_GOLD), 4)
    addCentered(p, makeLabel("Collect each core from defeated enemies. Equip one core at a time.", 12), 5)
    collectionButtons.clear()
    POWER_GEMS.foreach { gem =>
      addCentered(p, makeTitle(gem.name + "  ?  " + gem.powerName, 15, gem.tint), 16)
      addCentered(p, makeLabel(gem.description, 11, C_SOFT), 2)
      val b = makeButton("LOCKED  ?  DEFEAT ENEMIES TO FIND THIS CORE")
      collectionButtons += b
      addCentered(p, b, 5)
      addAction(b) { equipGem(gem.id); refreshCollection() }
    }
    collectionStatusArea = new JTextArea(3, 48)
    collectionStatusArea.setEditable(false)
    collectionStatusArea.setFocusable(false)
    collectionStatusArea.setBackground(C_PANEL)
    collectionStatusArea.setForeground(C_MINT)
    collectionStatusArea.setFont(new Font("Monospaced", Font.BOLD, 12))
    collectionStatusArea.setBorder(BorderFactory.createLineBorder(C_LINE))
    collectionStatusArea.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(12)); p.add(collectionStatusArea)
    val back = makeButton("RETURN")
    addCentered(p, back, 16)
    addAction(back) { returnFromCollection() }
    p
  }

  private def refreshCollection(): Unit = {
    var i = 0
    while (i < collectionButtons.length && i < POWER_GEMS.length) {
      val gem = POWER_GEMS(i)
      val owned = collectedGems.contains(gem.id)
      collectionButtons(i).setEnabled(owned)
      collectionButtons(i).setText(if (!owned) "LOCKED  ?  DEFEAT AN ENEMY" else if (equippedGem == gem.id) "EQUIPPED  ?  ACTIVE POWER" else "EQUIP THIS CORE")
      i += 1
    }
    if (collectionStatusArea != null) {
      val active = currentGem.map(_.name).getOrElse("NONE")
      collectionStatusArea.setText("COLLECTED: " + collectedGems.size + " / 5\nEQUIPPED: " + active + "\nIN GAME: PRESS 1-5 TO EQUIP  ?  PRESS R TO RELEASE POWER")
    }
  }

  // ---------------------------------------------------------------------------
  // GAME CANVAS AND CONTROLS
  // ---------------------------------------------------------------------------
  private final class RealmCanvas extends JPanel {
    setFocusable(true)
    setBackground(C_BG)
    setPreferredSize(new Dimension(1360, 820))
    setDoubleBuffered(true)

    addKeyListener(new KeyAdapter {
      override def keyPressed(e: KeyEvent): Unit = {
        val code = e.getKeyCode
        val fresh = !keys.contains(code)
        keys += code
        if (!fresh) return
        if (code == KeyEvent.VK_E) interact()
        else if (code == KeyEvent.VK_F) fight()
        else if (code == KeyEvent.VK_R) useEquippedPower()
        else if (code == KeyEvent.VK_C) showCollection(true)
        else if (code >= KeyEvent.VK_1 && code <= KeyEvent.VK_5) equipGem(code - KeyEvent.VK_1)
        else if (code == KeyEvent.VK_SPACE) doPulse()
        else if (code == KeyEvent.VK_SHIFT && dashCooldown <= 0.0 && started && !paused && !cleared && !failed) {
          dashTime = 0.24
          dashCooldown = 1.15
          burst(playerX, playerY, C_MINT, 9)
        }
        else if (code == KeyEvent.VK_P || code == KeyEvent.VK_ESCAPE) {
          if (!cleared && !failed) paused = !paused
        }
        else if (code == KeyEvent.VK_T) restartRealm()
        else if (code == KeyEvent.VK_O && isAdmin) openAdminPanel(true)
        else if (code == KeyEvent.VK_V) {
          cameraDistanceIndex = (cameraDistanceIndex + 1) % CAMERA_DISTANCES.length
          setToast("3D CAMERA: " + CAMERA_LABELS(cameraDistanceIndex), 1.5)
        }
        else if (code == KeyEvent.VK_M) returnToRealms()
        else if (code == KeyEvent.VK_ENTER && cleared) {
          if (activeRealm < realms.length) startRealm(activeRealm + 1)
          else returnToRealms()
        }
      }
      override def keyReleased(e: KeyEvent): Unit = keys -= e.getKeyCode
    })

    addMouseListener(new java.awt.event.MouseAdapter {
      override def mousePressed(e: java.awt.event.MouseEvent): Unit = requestFocusInWindow()
    })

    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)
        val w = getWidth
        val h = getHeight
        cameraX = clamp(playerX - w / 2.0, 0.0, math.max(0.0, WORLD_W - w))
        cameraY = clamp(playerY - h / 2.0, 0.0, math.max(0.0, WORLD_H - h))
        // Render the 3D perspective world first, then keep the existing HUD and map in 2D.
        draw3DScene(g, w, h)
        drawMiniMap(g, w)
        drawHUD(g, w, h)
      } finally g.dispose()
    }
  }

  // ---------------------------------------------------------------------------
  // SCREEN: LOGIN
  // ---------------------------------------------------------------------------
  private def buildLoginScreen(): JPanel = {
    val p = verticalPanel()
    p.add(Box.createVerticalGlue())
    addCentered(p, makeTitle("SANCTUS", 23, C_MINT), 5)
    addCentered(p, makeTitle("SPRING SEASON  /  FOURTH EVENT", 31, C_WHITE), 4)
    addCentered(p, makeTitle("THE LUMEN CIRCUIT", 17, C_GOLD), 3)
    addCentered(p, makeLabel("Create a field identity to enter the floating realms.", 14), 17)

    val form = new JPanel(new java.awt.GridBagLayout())
    form.setOpaque(false)
    val c = new java.awt.GridBagConstraints()
    c.insets = new java.awt.Insets(8, 8, 8, 8)
    c.fill = java.awt.GridBagConstraints.HORIZONTAL
    c.gridx = 0; c.gridy = 0
    val n = new JLabel("FIELD NAME")
    n.setForeground(C_WHITE)
    n.setFont(new Font("Dialog", Font.BOLD, 12))
    form.add(n, c)
    c.gridx = 1
    loginNameField = new JTextField("PLAYER", 20)
    loginNameField.setFont(new Font("Dialog", Font.PLAIN, 14))
    form.add(loginNameField, c)
    c.gridx = 0; c.gridy = 1
    val pin = new JLabel("ACCESS KEY")
    pin.setForeground(C_WHITE)
    pin.setFont(new Font("Dialog", Font.BOLD, 12))
    form.add(pin, c)
    c.gridx = 1
    loginPinField = new JPasswordField(20)
    form.add(loginPinField, c)
    p.add(Box.createVerticalStrut(8))
    form.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(form)

    val enter = makeButton("ENTER THE CIRCUIT")
    addCentered(p, enter, 16)
    addCentered(p, makeLabel("Authorized control access is checked privately.", 11, new Color(126, 151, 178)), 8)
    p.add(Box.createVerticalGlue())

    addAction(enter) {
      val typed = Option(loginNameField.getText).getOrElse("").trim.replaceAll("[,;|\\r\\n]", "_").take(18)
      val safe = if (typed.isEmpty) "PLAYER" else typed
      val pinText = new String(loginPinField.getPassword)
      isAdmin = safe.equalsIgnoreCase(ADMIN_NAME) && pinText == ADMIN_PIN
      playerName = safe
      loginPinField.setText("")
      updateWelcome()
      if (adminMenuButton != null) adminMenuButton.setVisible(isAdmin)
      showCard("menu")
    }
    p
  }

  // ---------------------------------------------------------------------------
  // SCREEN: MAIN HUB
  // ---------------------------------------------------------------------------
  private def buildMenuScreen(): JPanel = {
    val p = verticalPanel()
    p.add(Box.createVerticalGlue())
    addCentered(p, makeTitle("SANCTUS EVENT", 35, C_MINT), 2)
    addCentered(p, makeTitle("SPRING SEASON  /  FOURTH EVENT", 16, C_GOLD), 3)
    welcomeLabel = makeLabel("Welcome, PLAYER", 14, C_SOFT)
    addCentered(p, welcomeLabel, 18)
    addCentered(p, makeLabel("Fight enemies. Claim five cores. Master one power at a time.", 13, C_BLUE), 5)

    val realmsButton = makeButton("OPEN THE REALM GATE")
    val setupButton = makeButton("EXPEDITION SETTINGS")
    val collectionButton = makeButton("POWER GEM COLLECTION")
    val recordsButton = makeButton("FIELD RECORDS")
    hostButton = makeButton("HOST LAN MULTIPLAYER")
    joinButton = makeButton("JOIN LAN MULTIPLAYER")
    adminMenuButton = makeButton("CONTROL SANCTUM")
    val logoutButton = makeButton("LEAVE FIELD")
    addCentered(p, realmsButton, 14)
    addCentered(p, setupButton, 7)
    addCentered(p, collectionButton, 7)
    addCentered(p, recordsButton, 7)
    addCentered(p, hostButton, 7)
    addCentered(p, joinButton, 7)
    addCentered(p, adminMenuButton, 7)
    addCentered(p, logoutButton, 7)
    adminMenuButton.setVisible(false)
    p.add(Box.createVerticalGlue())

    addAction(realmsButton) { refreshRealmButtons(); showCard("realms") }
    addAction(setupButton) { showCard("settings") }
    addAction(collectionButton) { showCollection(false) }
    addAction(recordsButton) { showRecords() }
    addAction(hostButton) { hostLanGame() }
    addAction(joinButton) { joinLanGame() }
    addAction(adminMenuButton) { openAdminPanel(false) }
    addAction(logoutButton) {
      stopGame()
      closeNetwork()
      playerName = "PLAYER"
      isAdmin = false
      loginNameField.setText("PLAYER")
      loginPinField.setText("")
      adminMenuButton.setVisible(false)
      showCard("login")
    }
    p
  }

  private def updateWelcome(): Unit = {
    if (welcomeLabel != null) welcomeLabel.setText("Welcome, " + playerName + (if (isAdmin) "  ?  CONTROLLER" else "  ?  EXPLORER"))
  }

  // ---------------------------------------------------------------------------
  // SCREEN: REALM SELECT
  // ---------------------------------------------------------------------------
  private def buildRealmsScreen(): JPanel = {
    val p = verticalPanel()
    addCentered(p, makeTitle("CHOOSE A REALM", 29, C_MINT), 3)
    addCentered(p, makeLabel("Each cleared realm opens the next gate.", 13), 4)
    realmButtons.clear()
    var i = 0
    while (i < realms.length) {
      val index = i + 1
      addCentered(p, makeTitle("0" + index + "  /  " + realms(i).name, 17, if (i % 2 == 0) C_GOLD else C_BLUE), 16)
      addCentered(p, makeLabel(realms(i).caption, 12, C_SOFT), 3)
      val b = makeButton("ENTER REALM " + index)
      realmButtons += b
      addCentered(p, b, 6)
      addCentered(p, makeLabel("SIGILS REQUIRED: " + realms(i).relics + "     ?     SENTRIES: " + realms(i).sentries, 10, new Color(136, 163, 189)), 2)
      i += 1
    }
    val back = makeButton("RETURN TO HUB")
    addCentered(p, back, 18)
    p.add(Box.createVerticalGlue())
    i = 0
    while (i < realmButtons.length) {
      val index = i + 1
      val b = realmButtons(i)
      addAction(b) { startRealm(index) }
      i += 1
    }
    addAction(back) { showCard("menu") }
    refreshRealmButtons()
    p
  }

  private def refreshRealmButtons(): Unit = {
    var i = 0
    while (i < realmButtons.length) {
      val number = i + 1
      val complete = completedRealms.contains(number)
      val open = number == 1 || completedRealms.contains(number - 1)
      realmButtons(i).setEnabled(open)
      realmButtons(i).setText(if (complete) "REVISIT REALM " + number + "  ?  STABILIZED" else if (open) "ENTER REALM " + number else "REALM " + number + "  ?  SEALED")
      i += 1
    }
  }

  // ---------------------------------------------------------------------------
  // SCREEN: SETTINGS
  // ---------------------------------------------------------------------------
  private def buildSettingsScreen(): JPanel = {
    val p = verticalPanel()
    p.add(Box.createVerticalGlue())
    addCentered(p, makeTitle("EXPEDITION SETTINGS", 29, C_MINT), 4)
    addCentered(p, makeLabel("Tune the field before opening a realm.", 13), 8)

    val dRow = new JPanel()
    dRow.setOpaque(false)
    val dLabel = new JLabel("FIELD RATING   ")
    dLabel.setForeground(C_WHITE)
    dLabel.setFont(new Font("Dialog", Font.BOLD, 12))
    val difficultyBox = makeCombo(DIFFICULTIES, selectedDifficulty)
    dRow.add(dLabel); dRow.add(difficultyBox)
    dRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(18)); p.add(dRow)

    val mRow = new JPanel()
    mRow.setOpaque(false)
    val mLabel = new JLabel("RUN FORMAT   ")
    mLabel.setForeground(C_WHITE)
    mLabel.setFont(new Font("Dialog", Font.BOLD, 12))
    val modeBox = makeCombo(GAME_MODES, selectedMode)
    mRow.add(mLabel); mRow.add(modeBox)
    mRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(12)); p.add(mRow)

    val descriptions = new JTextArea("EASY       - first enemy is gentler; longer timer\nMEDIUM     - stronger enemies and more health\nHARD       - faster enemies and higher damage\nEXTREME    - large enemy health and tighter timer\nNIGHTMARE  - maximum pressure\n\nADVENTURE  - normal realm progress\nTIME ATTACK - shorter clock\nGEM HUNT   - extra sigils and enemy drops\nFREE ROAM  - no timer or enemies\n\nF: FIGHT   R: GEM POWER   C: COLLECTION   1-5: EQUIP   V: 3D CAMERA DISTANCE")
    descriptions.setEditable(false)
    descriptions.setFocusable(false)
    descriptions.setFont(new Font("Monospaced", Font.PLAIN, 12))
    descriptions.setForeground(C_SOFT)
    descriptions.setBackground(C_PANEL)
    descriptions.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(C_LINE), BorderFactory.createEmptyBorder(12, 15, 12, 15)))
    descriptions.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(20)); p.add(descriptions)

    val save = makeButton("APPLY SETTINGS")
    val back = makeButton("RETURN TO HUB")
    addCentered(p, save, 18)
    addCentered(p, back, 8)
    p.add(Box.createVerticalGlue())
    addAction(save) {
      selectedDifficulty = difficultyBox.getSelectedItem.toString
      selectedMode = modeBox.getSelectedItem.toString
      JOptionPane.showMessageDialog(frame, "Expedition settings applied.", "Settings", JOptionPane.INFORMATION_MESSAGE)
    }
    addAction(back) { showCard("menu") }
    p
  }

  // ---------------------------------------------------------------------------
  // SCREEN: CONTROL SANCTUM
  // ---------------------------------------------------------------------------
  private def buildAdminScreen(): JPanel = {
    val p = verticalPanel()
    addCentered(p, makeTitle("CONTROL SANCTUM", 29, C_GOLD), 4)
    addCentered(p, makeLabel("Press O in a realm to open this panel at any time; select a player or ALL PLAYERS.", 12), 4)

    val titleRow = new JPanel(); titleRow.setOpaque(false)
    val titleLabel = new JLabel("EVENT TITLE"); titleLabel.setForeground(C_WHITE)
    adminTitleField = new JTextField(eventTitle, 24)
    titleRow.add(titleLabel); titleRow.add(adminTitleField)
    titleRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(12)); p.add(titleRow)

    val subRow = new JPanel(); subRow.setOpaque(false)
    val subLabel = new JLabel("EVENT LINE"); subLabel.setForeground(C_WHITE)
    adminSubtitleField = new JTextField(eventSubtitle, 24)
    subRow.add(subLabel); subRow.add(adminSubtitleField)
    subRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(8)); p.add(subRow)

    val settingRow = new JPanel(); settingRow.setOpaque(false)
    adminDifficultyBox = makeCombo(DIFFICULTIES, selectedDifficulty)
    adminModeBox = makeCombo(GAME_MODES, selectedMode)
    settingRow.add(new JLabel("DIFFICULTY")); settingRow.add(adminDifficultyBox)
    settingRow.add(new JLabel("MODE")); settingRow.add(adminModeBox)
    settingRow.getComponents.foreach { case l: JLabel => l.setForeground(C_WHITE); l.setFont(new Font("Dialog", Font.BOLD, 11)); case _ => () }
    settingRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(8)); p.add(settingRow)

    val targetRow = new JPanel(); targetRow.setOpaque(false)
    val targetLabel = new JLabel("TARGET PLAYER"); targetLabel.setForeground(C_WHITE)
    adminTargetBox = new JComboBox[String]()
    adminTargetBox.setPreferredSize(new Dimension(310, 28))
    targetRow.add(targetLabel); targetRow.add(adminTargetBox)
    targetRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(14)); p.add(targetRow)

    val commandPanel = new JPanel(new java.awt.GridLayout(2, 3, 8, 8))
    commandPanel.setOpaque(false)
    val heal = makeButton("HEAL")
    val gems = makeButton("GRANT 5 GEMS")
    val boost = makeButton("SPEED BOOST")
    val freeze = makeButton("FREEZE ENEMIES")
    val clear = makeButton("CLEAR ENEMIES")
    val revive = makeButton("REVIVE")
    commandPanel.add(heal); commandPanel.add(gems); commandPanel.add(boost)
    commandPanel.add(freeze); commandPanel.add(clear); commandPanel.add(revive)
    commandPanel.setMaximumSize(new Dimension(720, 104))
    commandPanel.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(10)); p.add(commandPanel)
    addAction(heal) { sendAdminAction("HEAL") }
    addAction(gems) { sendAdminAction("GEMS") }
    addAction(boost) { sendAdminAction("BOOST") }
    addAction(freeze) { sendAdminAction("FREEZE") }
    addAction(clear) { sendAdminAction("CLEAR") }
    addAction(revive) { sendAdminAction("REVIVE") }

    adminStatusArea = new JTextArea(10, 55)
    adminStatusArea.setEditable(false)
    adminStatusArea.setFont(new Font("Monospaced", Font.PLAIN, 12))
    adminStatusArea.setForeground(C_MINT)
    adminStatusArea.setBackground(new Color(4, 13, 30))
    adminStatusArea.setBorder(BorderFactory.createLineBorder(C_LINE))
    val scroller = new JScrollPane(adminStatusArea)
    scroller.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(12)); p.add(scroller)

    val save = makeButton("SAVE EVENT CONFIGURATION")
    val refresh = makeButton("REFRESH PLAYER LIST")
    val back = makeButton("RETURN TO HUB")
    addCentered(p, save, 12); addCentered(p, refresh, 7); addCentered(p, back, 7)
    p.add(Box.createVerticalGlue())
    addAction(save) {
      val t = Option(adminTitleField.getText).getOrElse("").trim
      val sub = Option(adminSubtitleField.getText).getOrElse("").trim
      if (t.nonEmpty) eventTitle = t.take(48)
      if (sub.nonEmpty) eventSubtitle = sub.take(64)
      selectedDifficulty = adminDifficultyBox.getSelectedItem.toString
      selectedMode = adminModeBox.getSelectedItem.toString
      if (started) {
        // Keep the host and LAN clients on the same realm configuration.
        buildRealm()
        if (gameCanvas != null) gameCanvas.requestFocusInWindow()
      }
      refreshAdmin()
      JOptionPane.showMessageDialog(frame, "Configuration saved for this session." + (if (started) " The current realm has been reset to apply the new settings." else ""), "Control Sanctum", JOptionPane.INFORMATION_MESSAGE)
    }
    addAction(refresh) { refreshAdmin() }
    addAction(back) { closeAdminPanel() }
    refreshAdmin()
    p
  }

  private def refreshAdminTargets(): Unit = {
    if (adminTargetBox != null) {
      val desired = Vector("ALL PLAYERS") ++ networkPlayers.map(peer => peer.id + "  ?  " + peer.name)
      val current = (0 until adminTargetBox.getItemCount).map(i => adminTargetBox.getItemAt(i)).toVector
      // Do not rebuild the combo while its popup is open unless the roster changed.
      if (current != desired) {
        val old = Option(adminTargetBox.getSelectedItem).map(_.toString).getOrElse("ALL PLAYERS")
        adminTargetBox.removeAllItems()
        desired.foreach(item => adminTargetBox.addItem(item))
        val idx = (0 until adminTargetBox.getItemCount).find(i => adminTargetBox.getItemAt(i) == old).getOrElse(0)
        adminTargetBox.setSelectedIndex(idx)
      }
    }
  }

  private def refreshAdmin(): Unit = {
    if (adminTitleField != null) adminTitleField.setText(eventTitle)
    if (adminSubtitleField != null) adminSubtitleField.setText(eventSubtitle)
    if (adminDifficultyBox != null) adminDifficultyBox.setSelectedItem(selectedDifficulty)
    if (adminModeBox != null) adminModeBox.setSelectedItem(selectedMode)
    refreshAdminTargets()
    if (adminStatusArea != null) {
      val rows = ArrayBuffer[String]()
      rows += "SPRING SEASON  /  FOURTH EVENT"
      rows += "EVENT TITLE : " + eventTitle
      rows += "CONTROLLER  : " + (if (isAdmin) ADMIN_NAME else "PLAYER")
      rows += "RATING      : " + selectedDifficulty
      rows += "FORMAT      : " + selectedMode
      rows += "NETWORK     : " + multiplayerMode
      rows += "CONNECTED   : " + networkPlayers.length + " PLAYER(S)"
      rows += ""
      rows += "CONNECTED PLAYER ROSTER"
      if (networkPlayers.isEmpty) rows += "No LAN players in this session. Host a room or join one from the hub."
      networkPlayers.foreach { peer => rows += (peer.id + "  " + peer.name + "  HP " + peer.hp + "  REALM " + peer.realm) }
      rows += ""
      rows += "ADMIN ACTIONS: HEAL / GEMS / BOOST / FREEZE / CLEAR / REVIVE"
      rows += "Changes remain active until the application closes."
      adminStatusArea.setText(rows.mkString("\n"))
    }
  }

  // ---------------------------------------------------------------------------
  // FIELD RECORDS
  // ---------------------------------------------------------------------------
  private def showRecords(): Unit = {
    val text = new StringBuilder
    text.append("SANCTUS EVENT  /  FIELD RECORDS\n\n")
    text.append("Explorer: ").append(playerName).append("\n")
    text.append("Stabilized realms: ").append(completedRealms.size).append(" / ").append(realms.length).append("\n\n")
    realms.indices.foreach { i =>
      text.append("0").append(i + 1).append("  ").append(realms(i).name).append("  ?  ")
      text.append(if (completedRealms.contains(i + 1)) "STABILIZED" else "NOT YET STABILIZED").append("\n")
    }
    text.append("\nPower gems collected: ").append(collectedGems.size).append(" / 5")
    text.append("\nEnemy defeats this realm: ").append(enemyKills)
    text.append("\nEquipped core: ").append(currentGem.map(_.name).getOrElse("NONE"))
    text.append("\nCurrent rating: ").append(selectedDifficulty)
    text.append("\nCurrent format: ").append(selectedMode)
    JOptionPane.showMessageDialog(frame, text.toString, "Field Records", JOptionPane.INFORMATION_MESSAGE)
  }

  // ---------------------------------------------------------------------------
  // WINDOW INITIALIZATION
  // ---------------------------------------------------------------------------
  private def buildUI(): Unit = {
    cardLayout = new java.awt.CardLayout()
    root = new JPanel(cardLayout)
    root.setBackground(C_BG)
    root.add(scrollCard(buildLoginScreen()), "login")
    root.add(scrollCard(buildMenuScreen()), "menu")
    root.add(scrollCard(buildRealmsScreen()), "realms")
    root.add(scrollCard(buildCollectionScreen()), "collection")
    root.add(scrollCard(buildSettingsScreen()), "settings")
    root.add(scrollCard(buildAdminScreen()), "admin")

    frame = new JFrame(APP_TITLE)
    frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE)
    frame.setContentPane(root)
    frame.addWindowListener(new WindowAdapter {
      override def windowClosing(e: WindowEvent): Unit = {
        stopGame()
        if (networkAdminTimer != null) networkAdminTimer.stop()
        closeNetwork()
        frame.dispose()
      }
    })
    frame.setMinimumSize(new Dimension(900, 650))
    frame.setSize(1180, 790)
    frame.setLocationRelativeTo(null)
    frame.setVisible(true)
    networkAdminTimer = new Timer(120, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        consumeNetworkAdminCommands()
        consumeNetworkActions()
        if (adminStatusArea != null && root != null && cardLayout != null) {
          // Refresh only the target-player list; do not overwrite text being edited.
          if (adminTargetBox != null && adminTargetBox.isShowing) refreshAdminTargets()
        }
      }
    })
    networkAdminTimer.start()
    showCard("login")
  }

  def main(args: Array[String]): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = buildUI()
    })
  }
}


// ===== INTEGRATED GAME MODULE: Sanctus_Fourth_Event_Combat_Multiplayer.scala =====

import java.awt.{AlphaComposite, BasicStroke, BorderLayout, Color, Component, Dimension, Font, GradientPaint, Graphics, Graphics2D, RenderingHints}
import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, WindowAdapter, WindowEvent}
import java.io.{BufferedReader, InputStreamReader, PrintWriter}
import java.net.{InetAddress, InetSocketAddress, ServerSocket, Socket}
import java.util.concurrent.{ConcurrentHashMap, ConcurrentLinkedQueue}
import java.util.concurrent.atomic.AtomicInteger
import java.awt.geom.{Ellipse2D, Path2D}
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

/**
 * SPRING SEASON - FOURTH EVENT
 * SANCTUS EVENT
 * Stand-alone Scala + Java Swing game. No external assets or libraries required.
 * Admin account: BIXUU@GC. The existing PIN is checked but never displayed in the UI.
 */
object SanctusFourthEventCombat {

  // ---------------------------------------------------------------------------
  // APPLICATION SETTINGS
  // ---------------------------------------------------------------------------
  private val APP_TITLE = "SANCTUS EVENT | SPRING SEASON 04"
  private val ADMIN_NAME = "BIXUU@GC"
  // Retains the PIN supplied in the earlier project; it is intentionally not shown in the UI.
  private val ADMIN_PIN = "BIXUU-2026"

  private val WORLD_W = 1900.0
  private val WORLD_H = 1320.0
  private val PORTAL_X = 1710.0
  private val PORTAL_Y = 175.0
  private val SPAWN_X = 135.0
  private val SPAWN_Y = 1130.0

  private val C_BG = new Color(7, 12, 29)
  private val C_PANEL = new Color(13, 25, 48)
  private val C_PANEL_2 = new Color(20, 39, 65)
  private val C_LINE = new Color(74, 117, 150)
  private val C_WHITE = new Color(241, 248, 255)
  private val C_SOFT = new Color(176, 198, 220)
  private val C_MINT = new Color(116, 245, 205)
  private val C_GOLD = new Color(255, 217, 133)
  private val C_PINK = new Color(255, 145, 205)
  private val C_BLUE = new Color(108, 194, 255)
  private val C_VIOLET = new Color(183, 143, 255)

  private val DIFFICULTIES = Array("EASY", "MEDIUM", "HARD", "EXTREME", "NIGHTMARE")
  private val GAME_MODES = Array("ADVENTURE", "TIME ATTACK", "GEM HUNT", "FREE ROAM")
  private val NETWORK_PORT = 5050
  private final case class GemInfo(id: Int, name: String, powerName: String, tint: Color, description: String)
  private val POWER_GEMS: Vector[GemInfo] = Vector(
    GemInfo(0, "RUBY CORE", "Flame Arc", new Color(255, 76, 91), "A focused blast that damages nearby enemies."),
    GemInfo(1, "SAPPHIRE CORE", "Frost Lock", new Color(75, 190, 255), "Freezes nearby enemies for a short time."),
    GemInfo(2, "EMERALD CORE", "Life Bloom", new Color(81, 226, 142), "Restores health and one charge."),
    GemInfo(3, "AMETHYST CORE", "Nova Burst", new Color(190, 112, 255), "Releases a powerful ring of energy."),
    GemInfo(4, "TOPAZ CORE", "Time Bend", new Color(255, 205, 76), "Slows every enemy for a few seconds.")
  )
  // Use Vector values before indexing; direct Array(...)(index) can be parsed as
  // a second argument list for Array.apply on some Scala/Kojo compiler versions.
  private val RELIC_NAMES: Vector[String] = Vector(
    "LUMEN SIGIL", "ORBIT GLASS", "DAWN SEAL",
    "AETHER MARK", "SOLAR TOKEN", "VEIL KEY"
  )
  private val DIFFICULTY_TIME: Vector[Int] = Vector(100, 35, -20, -65, -105)

  private final case class RealmInfo(name: String, caption: String, relics: Int, sentries: Int, hue: Int, time: Int)
  private val realms = Array(
    RealmInfo("MOSSWIND ATRIUM", "A quiet maze of living stone and lantern vines", 5, 1, 145, 360),
    RealmInfo("OPAL TIDEWAY", "A glass coast above a slow-moving sky sea", 6, 2, 190, 345),
    RealmInfo("SOLARIS CANOPY", "A high garden suspended around a sunlit engine", 7, 3, 42, 330),
    RealmInfo("VIOLET SKYFORGE", "A floating foundry where the air carries light", 8, 3, 270, 315),
    RealmInfo("SANCTUM OF FIRST LIGHT", "The final chamber beyond the luminous veil", 9, 4, 315, 300)
  )

  // ---------------------------------------------------------------------------
  // GAME OBJECTS
  // ---------------------------------------------------------------------------
  private final class Relic(val x: Double, val y: Double, val tint: Color, val title: String) {
    var collected = false
  }


  private final class GemDrop(val x: Double, val y: Double, val gemId: Int) {
    var collected = false
  }

  private final class Sentry(
    val baseX: Double,
    val baseY: Double,
    val radius: Double,
    val pace: Double,
    val phase: Double,
    val tint: Color,
    val maxHealth: Int,
    val rank: Int
  ) {
    var x = baseX
    var y = baseY
    var disabled = 0.0
    var slowTime = 0.0
    var health = maxHealth
    var defeated = false
    var hitFlash = 0.0
  }

  private final case class NetPlayer(id: String, name: String, x: Double, y: Double, hp: Int, realm: Int)

  /** LAN room server. The room host owns the world; authenticated Admin clients may issue controller commands. */
  private final class LanServer(port: Int, hostName: String) {
    private val socket = new ServerSocket()
    private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
    private val counter = new AtomicInteger(0)
    private val writers = new ConcurrentHashMap[String, PrintWriter]()
    private val positions = new ConcurrentHashMap[String, String]()
    private val adminIds = new ConcurrentHashMap[String, String]()
    private val hostCommands = new ConcurrentLinkedQueue[String]()
    private val hostActions = new ConcurrentLinkedQueue[String]()
    @volatile private var roomRealm = 1
    @volatile private var roomDifficulty = "EASY"
    @volatile private var roomMode = "ADVENTURE"

    socket.setReuseAddress(true)
    socket.bind(new InetSocketAddress(port))
    positions.put("HOST", safeNetworkName(hostName) + ",135.0,1130.0,100,1")

    def start(): Unit = {
      val acceptThread = new Thread(new Runnable {
        override def run(): Unit = {
          while (running.get()) {
            try {
              val client = socket.accept()
              client.setTcpNoDelay(true)
              val worker = new Thread(new Runnable { override def run(): Unit = handleClient(client) })
              worker.setDaemon(true)
              worker.start()
            } catch {
              case _: java.net.SocketException => ()
              case _: Throwable => ()
            }
          }
        }
      })
      acceptThread.setDaemon(true)
      acceptThread.start()
    }

    private def handleClient(client: Socket): Unit = {
      var id = ""
      var output: PrintWriter = null
      try {
        val input = new BufferedReader(new InputStreamReader(client.getInputStream, "UTF-8"))
        output = new PrintWriter(client.getOutputStream, true)
        val first = input.readLine()
        if (first != null && first.startsWith("JOIN|")) {
          val fields = first.split("\\|", 3)
          val requestedName = safeNetworkName(if (fields.length > 1) fields(1) else "PLAYER")
          val token = if (fields.length > 2) fields(2) else ""
          id = "P" + counter.incrementAndGet()
          val authorized = requestedName.equalsIgnoreCase(ADMIN_NAME) && token == ADMIN_PIN
          writers.put(id, output)
          positions.put(id, requestedName + ",135.0,1130.0,100,1")
          if (authorized) adminIds.put(id, "1")
          output.println("WELCOME|" + id + "|" + (if (authorized) "ADMIN" else "PLAYER"))
          output.println("WORLD|" + roomRealm + "|" + roomDifficulty + "|" + roomMode)
          broadcastState()
          var line = input.readLine()
          while (line != null && running.get()) {
            if (line.startsWith("POS|")) {
              val values = line.substring(4).split(",", 4)
              if (values.length == 4) {
                try {
                  val old = positions.get(id)
                  val name = if (old == null) requestedName else old.takeWhile(_ != ',')
                  val px = clamp(values(0).toDouble, 0.0, WORLD_W)
                  val py = clamp(values(1).toDouble, 0.0, WORLD_H)
                  val hp = math.max(0, math.min(100, values(2).toInt))
                  val realm = math.max(1, math.min(realms.length, values(3).toInt))
                  positions.put(id, name + "," + px + "," + py + "," + hp + "," + realm)
                  broadcastState()
                } catch { case _: Throwable => () }
              }
            } else if (line.startsWith("ADMIN|") && adminIds.containsKey(id)) {
              val fields = line.split("\\|", 3)
              if (fields.length == 3) publishAdminCommand(fields(1), fields(2))
            } else if (line.startsWith("ACT|")) {
              val action = line.substring(4).take(100).replaceAll("[\\r\\n]", "_")
              hostActions.add(action)
              broadcastExcept(id, "ACT|" + action)
            }
            line = input.readLine()
          }
        }
      } catch { case _: Throwable => () }
      finally {
        if (id.nonEmpty) {
          writers.remove(id)
          positions.remove(id)
          adminIds.remove(id)
          broadcastState()
        }
        try client.close() catch { case _: Throwable => () }
      }
    }

    private def broadcast(message: String): Unit = {
      val it = writers.values().iterator()
      while (it.hasNext) {
        try { val out = it.next(); out.synchronized { out.println(message) } }
        catch { case _: Throwable => () }
      }
    }

    private def broadcastExcept(exceptId: String, message: String): Unit = {
      val it = writers.entrySet().iterator()
      while (it.hasNext) {
        val entry = it.next()
        if (entry.getKey != exceptId) {
          try { val out = entry.getValue; out.synchronized { out.println(message) } }
          catch { case _: Throwable => () }
        }
      }
    }

    private def broadcastState(): Unit = {
      val b = new StringBuilder("STATE|")
      val it = positions.entrySet().iterator()
      var first = true
      while (it.hasNext) {
        val e = it.next()
        if (!first) b.append(";")
        first = false
        b.append(e.getKey).append(",").append(e.getValue)
      }
      broadcast(b.toString)
    }

    private def publishAdminCommand(command: String, target: String): Unit = {
      val safe = command.replaceAll("[|;\\r\\n]", "_")
      val who = target.replaceAll("[|;\\r\\n]", "_")
      val wire = safe + "|" + who
      hostCommands.add(wire)
      broadcast("CMD|" + wire)
    }

    def publishHostPosition(x: Double, y: Double, hp: Int, realm: Int, difficulty: String, mode: String): Unit = {
      roomRealm = math.max(1, math.min(realms.length, realm))
      roomDifficulty = if (DIFFICULTIES.contains(difficulty)) difficulty else "EASY"
      roomMode = if (GAME_MODES.contains(mode)) mode else "ADVENTURE"
      positions.put("HOST", safeNetworkName(hostName) + "," + x + "," + y + "," + hp + "," + roomRealm)
      broadcastState()
      broadcast("WORLD|" + roomRealm + "|" + roomDifficulty + "|" + roomMode)
    }

    def broadcastHostCommand(command: String, target: String): Unit = publishAdminCommand(command, target)

    def consumeHostCommands(): Seq[String] = {
      val b = ArrayBuffer[String]()
      var value = hostCommands.poll()
      while (value != null) { b += value; value = hostCommands.poll() }
      b.toSeq
    }

    def broadcastHostAction(action: String): Unit = broadcast("ACT|" + action.take(100).replaceAll("[\\r\\n]", "_"))

    def consumeHostActions(): Seq[String] = {
      val b = ArrayBuffer[String]()
      var value = hostActions.poll()
      while (value != null) { b += value; value = hostActions.poll() }
      b.toSeq
    }

    def snapshot(): Seq[NetPlayer] = parseNetworkSnapshot(positions)

    def close(): Unit = {
      running.set(false)
      try socket.close() catch { case _: Throwable => () }
      val it = writers.values().iterator()
      while (it.hasNext) try it.next().close() catch { case _: Throwable => () }
      writers.clear(); positions.clear(); adminIds.clear(); hostActions.clear(); hostCommands.clear()
    }
  }

  /** LAN room client. Admin proof is only sent when the Admin has logged in locally. */
  private final class LanClient(host: String, port: Int, name: String, adminToken: String) {
    private val socket = new Socket()
    socket.connect(new InetSocketAddress(host, port), 3500)
    socket.setTcpNoDelay(true)
    private val output = new PrintWriter(socket.getOutputStream, true)
    private val input = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
    private val peers = new ConcurrentHashMap[String, String]()
    private val commands = new ConcurrentLinkedQueue[String]()
    private val actions = new ConcurrentLinkedQueue[String]()
    @volatile private var connected = true
    @volatile private var localId = ""
    @volatile private var serverConfirmedAdmin = false
    @volatile private var roomRealm = 1
    @volatile private var roomDifficulty = "EASY"
    @volatile private var roomMode = "ADVENTURE"

    output.println("JOIN|" + safeNetworkName(name) + "|" + adminToken)
    private val welcome = input.readLine()
    if (welcome == null || !welcome.startsWith("WELCOME|")) throw new java.io.IOException("The room did not accept the connection.")
    val welcomeParts = welcome.split("\\|", 3)
    localId = if (welcomeParts.length > 1) welcomeParts(1) else ""
    serverConfirmedAdmin = welcomeParts.length > 2 && welcomeParts(2) == "ADMIN"

    private val reader = new Thread(new Runnable {
      override def run(): Unit = {
        try {
          var line = input.readLine()
          while (line != null && connected) {
            if (line.startsWith("STATE|")) {
              peers.clear()
              val body = line.substring(6)
              if (body.nonEmpty) body.split(";").foreach { entry =>
                val parts = entry.split(",", 6)
                if (parts.length == 6) peers.put(parts(0), parts.drop(1).mkString(","))
              }
            } else if (line.startsWith("CMD|")) commands.add(line.substring(4))
            else if (line.startsWith("ACT|")) actions.add(line.substring(4))
            else if (line.startsWith("WORLD|")) {
              val cfg = line.split("\\|", 4)
              if (cfg.length == 4) {
                try {
                  val r = cfg(1).toInt
                  if (r >= 1 && r <= realms.length) roomRealm = r
                  if (DIFFICULTIES.contains(cfg(2))) roomDifficulty = cfg(2)
                  if (GAME_MODES.contains(cfg(3))) roomMode = cfg(3)
                } catch { case _: Throwable => () }
              }
            }
            line = input.readLine()
          }
        } catch { case _: Throwable => () }
        connected = false
      }
    })
    reader.setDaemon(true)
    reader.start()

    def id: String = localId
    def worldConfig: (Int, String, String) = (roomRealm, roomDifficulty, roomMode)
    def isAdminOnServer: Boolean = serverConfirmedAdmin
    def isConnected: Boolean = connected
    def sendPosition(x: Double, y: Double, hp: Int, realm: Int): Unit = if (connected) output.println("POS|" + x + "," + y + "," + hp + "," + realm)
    def sendAdminCommand(command: String, target: String): Unit = if (connected && serverConfirmedAdmin) output.println("ADMIN|" + command + "|" + target)
    def sendAction(action: String): Unit = if (connected) output.println("ACT|" + action.take(100).replaceAll("[\\r\\n]", "_"))
    def consumeActions(): Seq[String] = {
      val b = ArrayBuffer[String]()
      var value = actions.poll()
      while (value != null) { b += value; value = actions.poll() }
      b.toSeq
    }
    def consumeCommands(): Seq[String] = {
      val b = ArrayBuffer[String]()
      var value = commands.poll()
      while (value != null) { b += value; value = commands.poll() }
      b.toSeq
    }
    def snapshot(): Seq[NetPlayer] = parseNetworkSnapshot(peers)
    def close(): Unit = { connected = false; try socket.close() catch { case _: Throwable => () }; actions.clear(); commands.clear() }
  }

  private final class Bloom(val x: Double, val y: Double, val size: Double, val hue: Int, val phase: Double)
  private final class Star(val x: Double, val y: Double, val size: Int, val phase: Double)
  private final class Spark(var x: Double, var y: Double, var vx: Double, var vy: Double, val size: Double, val tint: Color, var life: Double)

  private def safeNetworkName(value: String): String = Option(value).getOrElse("PLAYER").trim.replaceAll("[,;|\\r\\n]", "_").take(18) match {
    case "" => "PLAYER"
    case other => other
  }

  private def parseNetworkSnapshot(source: ConcurrentHashMap[String, String]): Seq[NetPlayer] = {
    val result = ArrayBuffer[NetPlayer]()
    val it = source.entrySet().iterator()
    while (it.hasNext) {
      val e = it.next()
      val parts = e.getValue.split(",", 5)
      if (parts.length == 5) {
        try result += NetPlayer(e.getKey, parts(0), parts(1).toDouble, parts(2).toDouble, parts(3).toInt, parts(4).toInt)
        catch { case _: Throwable => () }
      }
    }
    result.toSeq
  }

  // ---------------------------------------------------------------------------
  // APPLICATION STATE
  // ---------------------------------------------------------------------------
  private var frame: JFrame = null
  private var root: JPanel = null
  private var cardLayout: java.awt.CardLayout = null
  private var gameCanvas: RealmCanvas = null
  private var gameTimer: Timer = null

  private var playerName = "PLAYER"
  private var isAdmin = false
  private var eventTitle = "SANCTUS EVENT"
  private var eventSubtitle = "THE LUMEN CIRCUIT"
  private var activeRealm = 1
  private var selectedDifficulty = "EASY"
  private var selectedMode = "ADVENTURE"
  private val completedRealms = mutable.Set[Int]()

  private val keys = mutable.Set[Int]()
  private val relics = ArrayBuffer[Relic]()
  private val sentries = ArrayBuffer[Sentry]()
  private val gemDrops = ArrayBuffer[GemDrop]()
  private val droppedGemIds = mutable.Set[Int]()
  private val collectedGems = mutable.Set[Int]()
  private val blooms = ArrayBuffer[Bloom]()
  private val stars = ArrayBuffer[Star]()
  private val sparks = ArrayBuffer[Spark]()
  private val rng = new java.util.Random(41042026L)

  private var playerX = SPAWN_X
  private var playerY = SPAWN_Y
  private var cameraX = 0.0
  private var cameraY = 0.0
  private var facingX = 0.0
  private var facingY = -1.0
  private var frameCount = 0L
  private var secondsLeft = 360.0
  private var charge = 4
  private var started = false
  private var paused = false
  private var cleared = false
  private var failed = false
  private var dashTime = 0.0
  private var dashCooldown = 0.0
  private var pulseCooldown = 0.0
  private var invulnerability = 0.0
  private var pulseFlash = 0.0
  private var playerHealth = 100
  private var attackCooldown = 0.0
  private var powerCooldown = 0.0
  private var attackFlash = 0.0
  private var speedBoostTime = 0.0
  private var equippedGem = -1
  private var enemyKills = 0
  private var multiplayerMode = "SOLO"
  private var lanServer: Option[LanServer] = None
  private var lanClient: Option[LanClient] = None
  private var lastNetworkFrame = 0L
  private var collectionFromGame = false
  private var collectionWasPaused = false
  private var adminFromGame = false
  private var adminWasPaused = false
  private var collectionButtons = ArrayBuffer[JButton]()
  private var collectionStatusArea: JTextArea = null
  private var adminTargetBox: JComboBox[String] = null
  private var toast = "Find the first enemy, fight with F, and claim its Ruby Core."
  private var toastTime = 4.0

  private var loginNameField: JTextField = null
  private var loginPinField: JPasswordField = null
  private var adminMenuButton: JButton = null
  private var welcomeLabel: JLabel = null
  private var realmButtons = ArrayBuffer[JButton]()
  private var adminTitleField: JTextField = null
  private var adminSubtitleField: JTextField = null
  private var adminDifficultyBox: JComboBox[String] = null
  private var adminModeBox: JComboBox[String] = null
  private var adminStatusArea: JTextArea = null
  private var hostButton: JButton = null
  private var joinButton: JButton = null
  private var networkAdminTimer: Timer = null

  // ---------------------------------------------------------------------------
  // GENERAL HELPERS
  // ---------------------------------------------------------------------------
  private def clamp(v: Double, lo: Double, hi: Double): Double = math.max(lo, math.min(hi, v))
  private def distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = {
    val dx = x1 - x2
    val dy = y1 - y2
    math.sqrt(dx * dx + dy * dy)
  }
  private def worldX(x: Double): Int = math.round(x - cameraX).toInt
  private def worldY(y: Double): Int = math.round(y - cameraY).toInt
  private def relicCount: Int = relics.count(_.collected)
  private def aliveEnemyCount: Int = sentries.count(s => !s.defeated)
  private def currentGem: Option[GemInfo] = POWER_GEMS.find(_.id == equippedGem)
  private def localNetworkId: String = if (multiplayerMode == "HOST") "HOST" else if (multiplayerMode == "JOIN") lanClient.map(_.id).getOrElse("") else "LOCAL"
  private def networkPlayers: Seq[NetPlayer] = multiplayerMode match {
    case "HOST" => lanServer.map(_.snapshot()).getOrElse(Seq.empty)
    case "JOIN" => lanClient.map(_.snapshot()).getOrElse(Seq.empty)
    case _ => Seq.empty
  }
  private def currentRealm: RealmInfo = realms(activeRealm - 1)
  private def colorFromHue(hue: Int, sat: Float = 0.68f, bright: Float = 1.0f): Color = Color.getHSBColor((hue.toDouble / 360.0).toFloat, sat, bright)
  private def setToast(message: String, duration: Double = 2.8): Unit = {
    toast = message
    toastTime = duration
  }

  private def makeButton(text: String): JButton = {
    val b = new JButton(text)
    b.setFont(new Font("Dialog", Font.BOLD, 14))
    b.setForeground(C_WHITE)
    b.setBackground(new Color(24, 59, 83))
    b.setFocusPainted(false)
    b.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR))
    b.setBorder(BorderFactory.createCompoundBorder(
      BorderFactory.createLineBorder(C_LINE, 1),
      BorderFactory.createEmptyBorder(10, 18, 10, 18)
    ))
    b.setAlignmentX(Component.CENTER_ALIGNMENT)
    b
  }

  private def makeTitle(text: String, size: Int, color: Color): JLabel = {
    val l = new JLabel(text, SwingConstants.CENTER)
    l.setFont(new Font("Dialog", Font.BOLD, size))
    l.setForeground(color)
    l.setAlignmentX(Component.CENTER_ALIGNMENT)
    l
  }

  private def makeLabel(text: String, size: Int = 13, color: Color = C_SOFT): JLabel = {
    val l = new JLabel(text, SwingConstants.CENTER)
    l.setFont(new Font("Dialog", Font.PLAIN, size))
    l.setForeground(color)
    l.setAlignmentX(Component.CENTER_ALIGNMENT)
    l
  }

  private def verticalPanel(): JPanel = {
    val p = new JPanel()
    p.setOpaque(true)
    p.setBackground(C_BG)
    p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS))
    p.setBorder(BorderFactory.createEmptyBorder(22, 28, 22, 28))
    p
  }

  private def addCentered(p: JPanel, c: JComponent, gap: Int = 9): Unit = {
    p.add(Box.createVerticalStrut(gap))
    c.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(c)
  }

  private def addAction(button: JButton)(action: => Unit): Unit = {
    button.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = action
    })
  }

  private def makeCombo(values: Array[String], selected: String): JComboBox[String] = {
    val c = new JComboBox[String](values)
    c.setSelectedItem(selected)
    c.setFont(new Font("Dialog", Font.BOLD, 13))
    c.setForeground(C_WHITE)
    c.setBackground(C_PANEL_2)
    c
  }

  private def showCard(name: String): Unit = {
    cardLayout.show(root, name)
    root.revalidate()
    root.repaint()
  }

  private def scrollCard(panel: JPanel): JScrollPane = {
    val s = new JScrollPane(panel)
    s.setBorder(BorderFactory.createEmptyBorder())
    s.getViewport.setBackground(C_BG)
    s.getVerticalScrollBar.setUnitIncrement(18)
    s
  }

  // ---------------------------------------------------------------------------
  // LEVEL CONSTRUCTION AND GAME FLOW
  // ---------------------------------------------------------------------------
  private def startRealm(number: Int, bypassUnlock: Boolean = false): Unit = {
    if (number < 1 || number > realms.length) return
    if (!bypassUnlock && number > 1 && !completedRealms.contains(number - 1)) {
      JOptionPane.showMessageDialog(frame, "Clear the previous realm before opening this gate.", "Gate sealed", JOptionPane.INFORMATION_MESSAGE)
      return
    }
    activeRealm = number
    buildRealm()
    if (gameTimer != null) gameTimer.stop()
    if (gameCanvas != null) root.remove(gameCanvas)
    gameCanvas = new RealmCanvas()
    root.add(gameCanvas, "game")
    showCard("game")
    gameTimer = new Timer(16, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        updateGame(0.016)
        if (gameCanvas != null) gameCanvas.repaint()
      }
    })
    gameTimer.start()
    gameCanvas.requestFocusInWindow()
  }

  private def buildRealm(): Unit = {
    relics.clear()
    sentries.clear()
    gemDrops.clear()
    droppedGemIds.clear()
    blooms.clear()
    stars.clear()
    sparks.clear()
    keys.clear()

    val info = currentRealm
    val difficultyIndex = DIFFICULTIES.indexOf(selectedDifficulty) match {
      case n if n >= 0 => n
      case _ => 1
    }
    val mode = selectedMode
    val extraRelics = if (mode == "GEM HUNT") 2 else 0
    val totalRelics = info.relics + extraRelics

    var i = 0
    while (i < totalRelics) {
      val x = 285.0 + ((i * 271 + activeRealm * 113) % 1310).toDouble
      val y = 260.0 + ((i * 187 + activeRealm * 79) % 810).toDouble
      val tint = colorFromHue((info.hue + i * 39 + 35) % 360)
      val name: String = RELIC_NAMES(i % RELIC_NAMES.length)
      relics += new Relic(x, y, tint, name)
      i += 1
    }

    val noHazards = mode == "FREE ROAM"
    val hazardCount = if (noHazards) 0 else math.max(1, info.sentries + math.max(0, difficultyIndex - 1))
    i = 0
    while (i < hazardCount) {
      val x = 470.0 + i * 330.0
      val y = 345.0 + (i % 2) * 390.0
      val tint = if (i % 2 == 0) C_PINK else C_VIOLET
      val hp = 46 + (activeRealm - 1) * 18 + difficultyIndex * 17 + i * 8
      val rank = 1 + activeRealm + difficultyIndex
      val pace = 0.58 + (activeRealm - 1) * 0.13 + difficultyIndex * 0.11
      sentries += new Sentry(x, y, 75.0 + difficultyIndex * 15.0, pace, i * 1.31, tint, hp, rank)
      i += 1
    }

    i = 0
    while (i < 250) {
      val x = 25.0 + rng.nextDouble() * (WORLD_W - 50.0)
      val y = 30.0 + rng.nextDouble() * (WORLD_H - 60.0)
      blooms += new Bloom(x, y, 3.0 + rng.nextDouble() * 8.0, (info.hue + rng.nextInt(130)) % 360, rng.nextDouble() * 6.28)
      i += 1
    }

    i = 0
    while (i < 180) {
      stars += new Star(rng.nextDouble() * WORLD_W, rng.nextDouble() * WORLD_H, 1 + rng.nextInt(3), rng.nextDouble() * 6.28)
      i += 1
    }

    playerX = SPAWN_X
    playerY = SPAWN_Y
    cameraX = 0.0
    cameraY = 0.0
    facingX = 0.0
    facingY = -1.0
    frameCount = 0L
    val difficultyTime: Int = DIFFICULTY_TIME(difficultyIndex)
    val modeTime = if (mode == "TIME ATTACK") -100 else if (mode == "GEM HUNT") -35 else 0
    secondsLeft = if (mode == "FREE ROAM") 99999.0 else math.max(100.0, info.time + difficultyTime + modeTime)
    charge = if (difficultyIndex >= 3) 3 else 4
    playerHealth = 100
    attackCooldown = 0.0
    powerCooldown = 0.0
    attackFlash = 0.0
    speedBoostTime = 0.0
    enemyKills = 0
    if (equippedGem < 0 && collectedGems.nonEmpty) equippedGem = collectedGems.toSeq.sorted.head
    started = true
    paused = false
    cleared = false
    failed = false
    dashTime = 0.0
    dashCooldown = 0.0
    pulseCooldown = 0.0
    invulnerability = 0.0
    pulseFlash = 0.0
    setToast("Realm opened: " + info.name + " | Fight with F. First enemy drops the Ruby Core.", 5.0)
  }

  private def stopGame(): Unit = {
    if (gameTimer != null) gameTimer.stop()
    gameTimer = null
    started = false
    keys.clear()
  }

  private def returnToRealms(): Unit = {
    stopGame()
    refreshRealmButtons()
    showCard("realms")
  }

  private def restartRealm(): Unit = {
    buildRealm()
    if (gameCanvas != null) gameCanvas.requestFocusInWindow()
  }

  private def dropGem(enemy: Sentry): Unit = {
    val next = POWER_GEMS.find(g => !collectedGems.contains(g.id) && !droppedGemIds.contains(g.id))
    next.foreach { gem =>
      droppedGemIds += gem.id
      gemDrops += new GemDrop(enemy.x, enemy.y, gem.id)
      burst(enemy.x, enemy.y, gem.tint, 25)
      setToast("ENEMY DEFEATED ? " + gem.name + " dropped! Move close and press E.", 3.8)
    }
  }

  private def defeatEnemy(enemy: Sentry): Unit = {
    if (!enemy.defeated) {
      enemy.defeated = true
      enemyKills += 1
      dropGem(enemy)
      burst(enemy.x, enemy.y, enemy.tint, 32)
    }
  }

  private def damageEnemy(enemy: Sentry, amount: Int): Unit = {
    if (!enemy.defeated) {
      enemy.health = math.max(0, enemy.health - amount)
      enemy.hitFlash = 0.22
      burst(enemy.x, enemy.y, enemy.tint, 7)
      if (enemy.health <= 0) defeatEnemy(enemy)
    }
  }

  private def fight(): Unit = {
    if (!started || paused || cleared || failed || attackCooldown > 0.0) return
    attackCooldown = 0.42
    attackFlash = 0.20
    val target = sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 132.0)
      .sortBy(s => distance(playerX, playerY, s.x, s.y)).headOption
    target match {
      case Some(enemy) =>
        val damage = 34 + activeRealm * 3 + (if (speedBoostTime > 0.0) 8 else 0)
        sharedDamageEnemy(enemy, damage)
        if (enemy.defeated) setToast("ENEMY DEFEATED ? a POWER GEM has dropped. Move close and press E.", 3.0)
        else setToast("GAUNTLET STRIKE  ?  " + damage + " DAMAGE  ?  ENEMY HP " + enemy.health + "/" + enemy.maxHealth, 1.5)
      case None => setToast("No enemy in range. Move closer, then press F to fight.", 1.5)
    }
  }

  private def useEquippedPower(): Unit = {
    if (!started || paused || cleared || failed) return
    if (equippedGem < 0 || !collectedGems.contains(equippedGem)) {
      setToast("NO GEM EQUIPPED ? defeat an enemy and collect the Ruby Core first.", 2.5)
      return
    }
    if (powerCooldown > 0.0) {
      setToast("POWER RECHARGING: " + math.ceil(powerCooldown).toInt + "s", 1.3)
      return
    }
    powerCooldown = 7.0
    val gem = POWER_GEMS(equippedGem)
    gem.id match {
      case 0 =>
        var hits = 0
        sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 300.0).foreach { enemy =>
          sharedDamageEnemy(enemy, 55 + activeRealm * 7); hits += 1
        }
        burst(playerX + facingX * 70.0, playerY + facingY * 70.0, gem.tint, 36)
        setToast("RUBY CORE  ?  FLAME ARC  ?  " + hits + " ENEMY/ENEMIES HIT", 2.2)
      case 1 =>
        var frozen = 0
        sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 460.0).foreach { enemy => enemy.disabled = 4.5; sendSharedAction("FREEZE|" + sentries.indexOf(enemy) + "|4500"); frozen += 1 }
        burst(playerX, playerY, gem.tint, 42)
        setToast("SAPPHIRE CORE  ?  FROST LOCK  ?  " + frozen + " ENEMY/ENEMIES FROZEN", 2.2)
      case 2 =>
        playerHealth = math.min(100, playerHealth + 42)
        charge = math.min(4, charge + 1)
        burst(playerX, playerY, gem.tint, 42)
        setToast("EMERALD CORE  ?  LIFE BLOOM  ?  HEALTH RESTORED", 2.2)
      case 3 =>
        var hits = 0
        sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 360.0).foreach { enemy =>
          sharedDamageEnemy(enemy, 76 + activeRealm * 8); hits += 1
        }
        burst(playerX, playerY, gem.tint, 65)
        setToast("AMETHYST CORE  ?  NOVA BURST  ?  " + hits + " ENEMY/ENEMIES HIT", 2.2)
      case 4 =>
        sentries.filterNot(_.defeated).foreach { enemy => enemy.slowTime = 6.0 }; sendSharedAction("SLOW|6000")
        burst(playerX, playerY, gem.tint, 55)
        setToast("TOPAZ CORE  ?  TIME BEND  ?  ALL ENEMIES SLOWED", 2.2)
      case _ => ()
    }
  }

  private def equipGem(id: Int): Unit = {
    if (id >= 0 && id < POWER_GEMS.length && collectedGems.contains(id)) {
      equippedGem = id
      val gem = POWER_GEMS(id)
      setToast("EQUIPPED: " + gem.name + "  ?  PRESS R TO USE " + gem.powerName, 2.4)
      refreshCollection()
    } else setToast("That gem has not been collected yet.", 1.6)
  }

  private def showCollection(fromGame: Boolean): Unit = {
    collectionFromGame = fromGame
    collectionWasPaused = paused
    if (fromGame) paused = true
    refreshCollection()
    showCard("collection")
  }

  private def returnFromCollection(): Unit = {
    if (collectionFromGame) {
      paused = collectionWasPaused
      showCard("game")
      if (gameCanvas != null) gameCanvas.requestFocusInWindow()
    } else showCard("menu")
  }

  private def openAdminPanel(fromGame: Boolean): Unit = {
    adminFromGame = fromGame
    adminWasPaused = paused
    if (fromGame) paused = true
    refreshAdmin()
    showCard("admin")
  }

  private def closeAdminPanel(): Unit = {
    if (adminFromGame) {
      paused = adminWasPaused
      showCard("game")
      if (gameCanvas != null) gameCanvas.requestFocusInWindow()
    } else showCard("menu")
    adminFromGame = false
  }

  private def sendSharedAction(action: String): Unit = {
    multiplayerMode match {
      case "HOST" => lanServer.foreach(_.broadcastHostAction(action))
      case "JOIN" => lanClient.foreach(_.sendAction(action))
      case _ => ()
    }
  }

  private def sharedDamageEnemy(enemy: Sentry, amount: Int): Unit = {
    val index = sentries.indexOf(enemy)
    damageEnemy(enemy, amount)
    if (index >= 0) sendSharedAction("HIT|" + index + "|" + amount)
  }

  private def collectGemId(id: Int): Unit = {
    if (id >= 0 && id < POWER_GEMS.length && !collectedGems.contains(id)) {
      collectedGems += id
      droppedGemIds -= id
      gemDrops.find(_.gemId == id).foreach(drop => drop.collected = true)
      if (equippedGem < 0) equippedGem = id
      val gem = POWER_GEMS(id)
      setToast("POWER GEM COLLECTED: " + gem.name + "  ?  PRESS " + (id + 1) + " TO EQUIP.", 3.4)
      refreshCollection()
    }
  }

  private def applySharedAction(wire: String): Unit = {
    val parts = wire.split("\\|", 4)
    if (parts.length >= 2) {
      parts(0) match {
        case "HIT" if parts.length >= 3 =>
          try {
            val idx = parts(1).toInt
            val amount = parts(2).toInt
            if (idx >= 0 && idx < sentries.length && amount >= 1 && amount <= 250) damageEnemy(sentries(idx), amount)
          } catch { case _: Throwable => () }
        case "GEM" => try collectGemId(parts(1).toInt) catch { case _: Throwable => () }
        case "SIGIL" =>
          try { val idx = parts(1).toInt; if (idx >= 0 && idx < relics.length) relics(idx).collected = true }
          catch { case _: Throwable => () }
        case "FREEZE" =>
          try {
            val idx = parts(1).toInt
            val rawDuration = if (parts.length >= 3) parts(2).toDouble / 1000.0 else 2.7
            val duration = clamp(rawDuration, 0.0, 12.0)
            if (idx >= 0 && idx < sentries.length && !sentries(idx).defeated)
              sentries(idx).disabled = math.max(sentries(idx).disabled, duration)
          } catch { case _: Throwable => () }
        case "SLOW" =>
          try {
            val duration = clamp(parts(1).toDouble / 1000.0, 0.0, 12.0)
            sentries.filterNot(_.defeated).foreach(enemy => enemy.slowTime = math.max(enemy.slowTime, duration))
          } catch { case _: Throwable => () }
        case _ => ()
      }
    }
  }

  private def consumeNetworkActions(): Unit = {
    val actions = if (multiplayerMode == "HOST") lanServer.map(_.consumeHostActions()).getOrElse(Seq.empty)
                  else if (multiplayerMode == "JOIN") lanClient.map(_.consumeActions()).getOrElse(Seq.empty)
                  else Seq.empty
    actions.foreach(applySharedAction)
  }

  private def applyAdminAction(command: String): Unit = {
    command match {
      case "HEAL" =>
        playerHealth = 100
        charge = math.min(4, math.max(1, charge))
        failed = false
        setToast("ADMIN CONTROL: HEALTH RESTORED.", 2.4)
      case "GEMS" =>
        POWER_GEMS.foreach(g => collectedGems += g.id)
        if (equippedGem < 0) equippedGem = 0
        refreshCollection()
        setToast("ADMIN CONTROL: ALL FIVE POWER GEMS GRANTED.", 2.4)
      case "BOOST" =>
        speedBoostTime = 12.0
        dashCooldown = 0.0
        setToast("ADMIN CONTROL: SPEED BOOST ENABLED.", 2.4)
      case "FREEZE" =>
        sentries.filterNot(_.defeated).foreach(enemy => enemy.disabled = 10.0)
        setToast("ADMIN CONTROL: ENEMIES FROZEN.", 2.4)
      case "CLEAR" =>
        sentries.foreach(s => if (!s.defeated) { s.health = 0; defeatEnemy(s) })
        setToast("ADMIN CONTROL: ENEMY WAVE CLEARED.", 2.4)
      case "REVIVE" =>
        playerHealth = 100
        charge = 4
        failed = false
        paused = false
        invulnerability = 2.5
        started = true
        setToast("ADMIN CONTROL: PLAYER REVIVED.", 2.4)
      case _ => setToast("Unknown Admin command.", 1.5)
    }
  }

  private def sendAdminAction(command: String): Unit = {
    if (!isAdmin) return
    val choice = if (adminTargetBox == null || adminTargetBox.getSelectedItem == null) "ALL" else adminTargetBox.getSelectedItem.toString
    val target = if (choice == "ALL PLAYERS") "ALL" else if (choice.startsWith("HOST  ?")) "HOST" else if (choice.contains("  ?  ")) choice.takeWhile(_ != ' ') else "ALL"
    multiplayerMode match {
      case "HOST" => lanServer match {
        case Some(server) => server.broadcastHostCommand(command, target)
        case None => applyAdminAction(command)
      }
      case "JOIN" => lanClient match {
        case Some(client) if client.isAdminOnServer => client.sendAdminCommand(command, target)
        case _ => JOptionPane.showMessageDialog(frame, "Admin multiplayer controls require an authorized room connection.", "Admin access", JOptionPane.WARNING_MESSAGE)
      }
      case _ => applyAdminAction(command)
    }
    refreshAdminTargets()
  }

  private def consumeNetworkAdminCommands(): Unit = {
    val commands = if (multiplayerMode == "HOST") lanServer.map(_.consumeHostCommands()).getOrElse(Seq.empty)
                   else if (multiplayerMode == "JOIN") lanClient.map(_.consumeCommands()).getOrElse(Seq.empty)
                   else Seq.empty
    commands.foreach { wire =>
      val parts = wire.split("\\|", 2)
      if (parts.length == 2) {
        val target = parts(1)
        val local = target == "ALL" || target == localNetworkId
        if (local) applyAdminAction(parts(0))
      }
    }
  }

  private def hostLanGame(): Unit = {
    val entered = JOptionPane.showInputDialog(frame, "Room port (default 5050):", String.valueOf(NETWORK_PORT))
    if (entered == null) return
    try {
      val port = entered.trim.toInt
      require(port >= 1024 && port <= 65535, "Port must be between 1024 and 65535.")
      closeNetwork()
      val server = new LanServer(port, playerName)
      server.start()
      lanServer = Some(server)
      multiplayerMode = "HOST"
      startRealm(1, true)
      val hostIp = try InetAddress.getLocalHost.getHostAddress catch { case _: Throwable => "Find this PC's IPv4 address in Network Settings" }
      JOptionPane.showMessageDialog(frame, "LAN room is running.\nHost address: " + hostIp + "\nPort: " + port + "\nOther players must use the same Wi-Fi or hotspot.", "Room hosted", JOptionPane.INFORMATION_MESSAGE)
      setToast("LAN ROOM HOSTED  ?  " + hostIp + ":" + port, 5.0)
    } catch {
      case ex: Throwable => JOptionPane.showMessageDialog(frame, "Could not host LAN room: " + ex.getMessage, "Room host error", JOptionPane.ERROR_MESSAGE)
    }
  }

  private def joinLanGame(): Unit = {
    val host = JOptionPane.showInputDialog(frame, "Host computer IP address:", "192.168.1.2")
    if (host == null || host.trim.isEmpty) return
    val entered = JOptionPane.showInputDialog(frame, "Room port:", String.valueOf(NETWORK_PORT))
    if (entered == null) return
    try {
      val port = entered.trim.toInt
      require(port >= 1024 && port <= 65535, "Port must be between 1024 and 65535.")
      closeNetwork()
      val token = if (isAdmin) ADMIN_PIN else ""
      val client = new LanClient(host.trim, port, playerName, token)
      lanClient = Some(client)
      multiplayerMode = "JOIN"
      if (isAdmin && !client.isAdminOnServer) JOptionPane.showMessageDialog(frame, "This room did not authorize the Admin account. You can play, but cannot control other players.", "Admin access", JOptionPane.INFORMATION_MESSAGE)
      startRealm(1, true)
      setToast("CONNECTED TO LAN ROOM  ?  " + host.trim + ":" + port, 4.0)
    } catch {
      case ex: Throwable => JOptionPane.showMessageDialog(frame, "Could not join LAN room: " + ex.getMessage, "Join error", JOptionPane.ERROR_MESSAGE)
    }
  }

  private def closeNetwork(): Unit = {
    lanClient.foreach(_.close()); lanClient = None
    lanServer.foreach(_.close()); lanServer = None
    multiplayerMode = "SOLO"
  }

  private def doPulse(): Unit = {
    if (!started || paused || cleared || failed || pulseCooldown > 0.0) return
    pulseCooldown = 4.2
    pulseFlash = 0.55
    var affected = 0
    sentries.foreach { s =>
      if (!s.defeated && distance(playerX, playerY, s.x, s.y) < 245.0) {
        s.disabled = 2.7
        sendSharedAction("FREEZE|" + sentries.indexOf(s) + "|2700")
        affected += 1
        burst(s.x, s.y, C_BLUE, 13)
      }
    }
    setToast(if (affected > 0) "LUMEN PULSE RELEASED ? nearby sentries paused." else "LUMEN PULSE RELEASED.", 2.0)
  }

  private def interact(): Unit = {
    if (!started || paused || cleared || failed) return
    val nearbyGem = gemDrops.filterNot(_.collected).sortBy(d => distance(playerX, playerY, d.x, d.y)).headOption
    nearbyGem match {
      case Some(drop) if distance(playerX, playerY, drop.x, drop.y) < 96.0 =>
        val gemId = drop.gemId
        val gem = POWER_GEMS(gemId)
        drop.collected = true
        collectGemId(gemId)
        sendSharedAction("GEM|" + gemId)
        burst(drop.x, drop.y, gem.tint, 35)
      case _ =>
        val nearest = relics.filterNot(_.collected).sortBy(r => distance(playerX, playerY, r.x, r.y)).headOption
        nearest match {
          case Some(r) if distance(playerX, playerY, r.x, r.y) < 94.0 =>
            r.collected = true
            sendSharedAction("SIGIL|" + relics.indexOf(r))
            burst(r.x, r.y, r.tint, 25)
            setToast("ACQUIRED: " + r.title + "   [" + relicCount + "/" + relics.length + "]", 2.2)
            if (relicCount == relics.length) setToast("ALL SIGILS SECURED ? travel to the far exit arch.", 4.0)
          case _ =>
            if (distance(playerX, playerY, PORTAL_X, PORTAL_Y) < 135.0) {
              if (relicCount == relics.length && aliveEnemyCount == 0) {
                cleared = true
                completedRealms += activeRealm
                burst(PORTAL_X, PORTAL_Y, C_GOLD, 80)
                setToast("REALM CLEARED ? " + currentRealm.name, 6.0)
              } else {
                val missing = relics.length - relicCount
                val remainingEnemies = aliveEnemyCount
                setToast("EXIT SEALED ? SIGILS LEFT: " + missing + "  ?  ENEMIES LEFT: " + remainingEnemies, 2.8)
              }
            } else setToast("Move close to a dropped gem, sigil, or exit arch, then press E.", 2.1)
        }
    }
  }

  private def burst(x: Double, y: Double, tint: Color, amount: Int): Unit = {
    var i = 0
    while (i < amount) {
      val a = rng.nextDouble() * math.Pi * 2.0
      val speed = 35.0 + rng.nextDouble() * 150.0
      sparks += new Spark(x, y, math.cos(a) * speed, math.sin(a) * speed, 2.0 + rng.nextDouble() * 4.0, tint, 0.65 + rng.nextDouble() * 1.2)
      i += 1
    }
  }

  private def updateGame(dt: Double): Unit = {
    frameCount += 1
    if (toastTime > 0.0) toastTime = math.max(0.0, toastTime - dt)
    if (dashTime > 0.0) dashTime = math.max(0.0, dashTime - dt)
    if (dashCooldown > 0.0) dashCooldown = math.max(0.0, dashCooldown - dt)
    if (pulseCooldown > 0.0) pulseCooldown = math.max(0.0, pulseCooldown - dt)
    if (invulnerability > 0.0) invulnerability = math.max(0.0, invulnerability - dt)
    if (pulseFlash > 0.0) pulseFlash = math.max(0.0, pulseFlash - dt)
    if (attackCooldown > 0.0) attackCooldown = math.max(0.0, attackCooldown - dt)
    if (powerCooldown > 0.0) powerCooldown = math.max(0.0, powerCooldown - dt)
    if (attackFlash > 0.0) attackFlash = math.max(0.0, attackFlash - dt)
    if (speedBoostTime > 0.0) speedBoostTime = math.max(0.0, speedBoostTime - dt)

    sparks.foreach { p =>
      p.x += p.vx * dt
      p.y += p.vy * dt
      p.vx *= 0.985
      p.vy *= 0.985
      p.life -= dt
    }
    sparks.filter(_.life <= 0.0).foreach(p => sparks -= p)
    while (sparks.length < 95) {
      sparks += new Spark(rng.nextDouble() * WORLD_W, WORLD_H - 8.0, (rng.nextDouble() - 0.5) * 10.0, -8.0 - rng.nextDouble() * 16.0, 1.5 + rng.nextDouble() * 2.4, C_MINT, 4.0)
    }

    sentries.foreach { s =>
      if (s.disabled > 0.0) s.disabled = math.max(0.0, s.disabled - dt)
      if (s.slowTime > 0.0) s.slowTime = math.max(0.0, s.slowTime - dt)
      if (s.hitFlash > 0.0) s.hitFlash = math.max(0.0, s.hitFlash - dt)
      val slowFactor = if (s.slowTime > 0.0) 0.20 else 1.0
      val t = frameCount * 0.021 * s.pace * slowFactor + s.phase
      // A frozen enemy really stops moving; a slowed enemy moves at 20% pace.
      if (!s.defeated && s.disabled <= 0.0) {
        s.x = s.baseX + math.sin(t) * s.radius
        s.y = s.baseY + math.cos(t * 0.72) * 34.0
      }
    }

    consumeNetworkAdminCommands()
    consumeNetworkActions()
    if (multiplayerMode == "HOST" && frameCount % 6 == 0L) {
      lanServer.foreach(_.publishHostPosition(playerX, playerY, playerHealth, activeRealm, selectedDifficulty, selectedMode))
    } else if (multiplayerMode == "JOIN" && frameCount % 6 == 0L) {
      lanClient.foreach(_.sendPosition(playerX, playerY, playerHealth, activeRealm))
      lanClient.foreach { client =>
        val config = client.worldConfig
        if (config._1 != activeRealm || config._2 != selectedDifficulty || config._3 != selectedMode) {
          activeRealm = config._1
          selectedDifficulty = config._2
          selectedMode = config._3
          buildRealm()
          setToast("ROOM SETTINGS UPDATED ? REALM " + activeRealm + ".", 3.0)
        }
      }
    }

    if (!started || paused || cleared || failed) return

    if (selectedMode != "FREE ROAM") {
      secondsLeft = math.max(0.0, secondsLeft - dt)
      if (secondsLeft <= 0.0) {
        failed = true
        setToast("TIME WINDOW CLOSED ? press T to retry this realm.", 6.0)
        return
      }
    }

    var mx = 0.0
    var my = 0.0
    if (keys.contains(KeyEvent.VK_W) || keys.contains(KeyEvent.VK_UP)) my -= 1.0
    if (keys.contains(KeyEvent.VK_S) || keys.contains(KeyEvent.VK_DOWN)) my += 1.0
    if (keys.contains(KeyEvent.VK_A) || keys.contains(KeyEvent.VK_LEFT)) mx -= 1.0
    if (keys.contains(KeyEvent.VK_D) || keys.contains(KeyEvent.VK_RIGHT)) mx += 1.0
    val length = math.sqrt(mx * mx + my * my)
    if (length > 0.0) {
      mx /= length
      my /= length
      facingX = mx
      facingY = my
      val speed = if (dashTime > 0.0) 465.0 else if (speedBoostTime > 0.0) 315.0 else 225.0
      playerX = clamp(playerX + mx * speed * dt, 35.0, WORLD_W - 35.0)
      playerY = clamp(playerY + my * speed * dt, 35.0, WORLD_H - 35.0)
    }

    sentries.foreach { enemy =>
      if (!enemy.defeated && enemy.disabled <= 0.0 && invulnerability <= 0.0 && distance(playerX, playerY, enemy.x, enemy.y) < 45.0) {
        playerHealth -= 18 + activeRealm * 2
        invulnerability = 0.90
        playerX = clamp(playerX - facingX * 54.0, 35.0, WORLD_W - 35.0)
        playerY = clamp(playerY - facingY * 54.0, 35.0, WORLD_H - 35.0)
        burst(playerX, playerY, C_PINK, 13)
        setToast("ENEMY HIT ? HEALTH " + math.max(0, playerHealth) + "/100", 1.7)
        if (playerHealth <= 0) {
          charge -= 1
          if (charge > 0) { playerHealth = 62; invulnerability = 2.0; setToast("CHARGE USED ? " + charge + " CHARGE(S) LEFT.", 2.4) }
          else { failed = true; setToast("NO CHARGES LEFT ? press T to retry this realm.", 5.0) }
        }
      }
    }

  }

  // ---------------------------------------------------------------------------
  // DRAWING UTILITIES
  // ---------------------------------------------------------------------------
  private def drawGlow(g: Graphics2D, x: Int, y: Int, radius: Int, color: Color, alpha: Float): Unit = {
    val old = g.getComposite
    g.setComposite(AlphaComposite.SrcOver.derive(alpha))
    g.setColor(color)
    g.fillOval(x - radius, y - radius, radius * 2, radius * 2)
    g.setComposite(old)
  }

  private def drawCentered(g: Graphics2D, text: String, x: Int, y: Int): Unit = {
    g.drawString(text, x - g.getFontMetrics.stringWidth(text) / 2, y)
  }

  private def drawBackdrop(g: Graphics2D, w: Int, h: Int): Unit = {
    val hue = currentRealm.hue
    val top = colorFromHue(hue, 0.73f, 0.22f)
    val bottom = new Color(8, 34, 49)
    g.setPaint(new GradientPaint(0f, 0f, top, 0f, h.toFloat, bottom))
    g.fillRect(0, 0, w, h)

    stars.foreach { s =>
      val x = worldX(s.x)
      val y = worldY(s.y)
      if (x >= 0 && y >= 0 && x < w && y < h) {
        val alpha = 95 + math.abs((math.sin(frameCount * 0.045 + s.phase) * 145).toInt)
        g.setColor(new Color(230, 247, 255, alpha))
        g.fillOval(x, y, s.size, s.size)
      }
    }

    val moonX = worldX(1470.0)
    val moonY = worldY(155.0)
    drawGlow(g, moonX, moonY, 115, C_GOLD, 0.12f)
    g.setColor(new Color(255, 229, 177))
    g.fillOval(moonX - 34, moonY - 34, 68, 68)
    g.setColor(new Color(255, 255, 255, 110))
    g.drawOval(moonX - 45, moonY - 45, 90, 90)
  }

  private def drawTerrain(g: Graphics2D, w: Int, h: Int): Unit = {
    val top = (h * 0.31).toInt
    g.setPaint(new GradientPaint(0f, top.toFloat, new Color(30, 104, 93), 0f, h.toFloat, new Color(5, 34, 46)))
    g.fillRect(0, top, w, h - top)
    val old = g.getComposite
    g.setComposite(AlphaComposite.SrcOver.derive(0.075f))
    g.setColor(C_MINT)
    var x = 0
    while (x < w) { g.drawLine(x, top, x, h); x += 78 }
    var y = top
    while (y < h) { g.drawLine(0, y, w, y); y += 78 }
    g.setComposite(old)

    // Floating garden shelves add depth without external images.
    var i = 0
    while (i < 12) {
      val wx = 130.0 + i * 157.0
      val wy = 215.0 + (i % 3) * 295.0
      val sx = worldX(wx)
      val sy = worldY(wy)
      if (sx > -170 && sx < w + 170 && sy > -100 && sy < h + 100) {
        g.setColor(new Color(6, 36, 47, 170))
        g.fillOval(sx - 65, sy - 8, 130, 42)
        g.setColor(new Color(71, 170, 134, 150))
        g.fillOval(sx - 62, sy - 15, 124, 29)
        g.setColor(new Color(168, 246, 213, 110))
        g.drawArc(sx - 42, sy - 17, 84, 20, 15, 145)
      }
      i += 1
    }
  }

  private def drawLandmark(g: Graphics2D): Unit = {
    val cx = worldX(960.0)
    val cy = worldY(600.0)
    val tone = colorFromHue(currentRealm.hue, 0.7f, 0.95f)
    drawGlow(g, cx, cy - 55, 190, tone, 0.10f)

    // New central landmark: a multi-ring lumen engine.
    g.setColor(new Color(13, 25, 49, 215))
    g.fillOval(cx - 120, cy - 93, 240, 186)
    g.setStroke(new BasicStroke(5.0f))
    g.setColor(tone)
    g.drawOval(cx - 105, cy - 115, 210, 210)
    g.setStroke(new BasicStroke(2.0f))
    g.setColor(new Color(232, 252, 255, 190))
    g.drawOval(cx - 82, cy - 91, 164, 164)

    var i = 0
    while (i < 9) {
      val a = frameCount * 0.012 + i * math.Pi * 2.0 / 9.0
      val px = cx + (math.cos(a) * 92).toInt
      val py = cy - 10 + (math.sin(a) * 92).toInt
      g.setColor(if (i % 2 == 0) C_GOLD else C_MINT)
      g.fillOval(px - 7, py - 7, 14, 14)
      i += 1
    }

    g.setColor(new Color(35, 77, 97))
    g.fillRoundRect(cx - 22, cy - 145, 44, 100, 15, 15)
    g.setColor(new Color(190, 240, 233))
    g.fillPolygon(Array(cx, cx + 18, cx, cx - 18), Array(cy - 182, cy - 139, cy - 95, cy - 139), 4)
    g.setFont(new Font("Dialog", Font.BOLD, 12))
    g.setColor(C_WHITE)
    drawCentered(g, "LUMEN ENGINE", cx, cy + 132)
  }

  private def drawBlooms(g: Graphics2D): Unit = {
    blooms.foreach { b =>
      val x = worldX(b.x)
      val y = worldY(b.y)
      if (x > -20 && y > -20 && x < 1940 && y < 1340) {
        val r = math.max(2, (b.size * (0.86 + 0.14 * math.sin(frameCount * 0.05 + b.phase))).toInt)
        g.setColor(new Color(40, 128, 88))
        g.drawLine(x, y + 2, x, y + r * 3)
        g.setColor(colorFromHue(b.hue))
        var k = 0
        while (k < 5) {
          val a = k * math.Pi * 2.0 / 5.0
          g.fillOval(x + (math.cos(a) * r * 0.72).toInt - r / 2, y + (math.sin(a) * r * 0.72).toInt - r / 2, r, r)
          k += 1
        }
        g.setColor(C_GOLD)
        g.fillOval(x - 2, y - 2, 4, 4)
      }
    }
  }

  private def drawRelic(g: Graphics2D, r: Relic): Unit = {
    if (r.collected) return
    val x = worldX(r.x)
    val y = worldY(r.y)
    if (x < -75 || y < -75 || x > 1975 || y > 1395) return
    val pulse = 13 + (3 * math.sin(frameCount * 0.075 + r.x)).toInt
    drawGlow(g, x, y, pulse * 3, r.tint, 0.13f)
    g.setColor(r.tint)
    g.fillPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
    g.setColor(C_WHITE)
    g.drawPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
    g.setFont(new Font("Dialog", Font.BOLD, 9))
    drawCentered(g, r.title, x, y - pulse - 14)
    g.setColor(C_MINT)
    drawCentered(g, "E: TAKE", x, y + pulse + 17)
  }

  private def drawSentry(g: Graphics2D, s: Sentry): Unit = {
    if (s.defeated) return
    val x = worldX(s.x)
    val y = worldY(s.y)
    if (x < -90 || y < -90 || x > 1980 || y > 1400) return
    val body = if (s.disabled > 0.0) C_BLUE else if (s.hitFlash > 0.0) C_WHITE else s.tint
    drawGlow(g, x, y, 52, body, if (s.disabled > 0.0) 0.10f else 0.15f)
    g.setColor(new Color(23, 30, 59))
    g.fillOval(x - 30, y - 27, 60, 54)
    g.setColor(body)
    g.fillOval(x - 25, y - 24, 50, 47)
    g.setColor(new Color(255, 250, 228))
    g.fillOval(x - 12, y - 6, 8, 9)
    g.fillOval(x + 5, y - 6, 8, 9)
    g.setColor(new Color(37, 42, 63))
    g.fillOval(x - 10, y - 3, 3, 4)
    g.fillOval(x + 8, y - 3, 3, 4)
    g.setColor(new Color(38, 48, 69))
    g.fillRoundRect(x - 27, y - 42, 54, 6, 4, 4)
    g.setColor(C_PINK)
    g.fillRoundRect(x - 27, y - 42, (54.0 * s.health / math.max(1, s.maxHealth)).toInt, 6, 4, 4)
    g.setFont(new Font("Dialog", Font.BOLD, 9))
    g.setColor(C_WHITE)
    drawCentered(g, if (s.disabled > 0.0) "FROZEN" else "ENEMY T" + s.rank, x, y - 50)
  }

  private def drawGemDrop(g: Graphics2D, drop: GemDrop): Unit = {
    if (drop.collected) return
    val gem = POWER_GEMS(drop.gemId)
    val x = worldX(drop.x)
    val y = worldY(drop.y)
    val pulse = 12 + (3 * math.sin(frameCount * 0.09 + drop.gemId)).toInt
    drawGlow(g, x, y, pulse * 3, gem.tint, 0.18f)
    g.setColor(gem.tint)
    g.fillPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
    g.setColor(C_WHITE)
    g.drawPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
    g.setFont(new Font("Dialog", Font.BOLD, 9))
    drawCentered(g, gem.name, x, y - pulse - 15)
    g.setColor(C_GOLD)
    drawCentered(g, "E: COLLECT", x, y + pulse + 17)
  }

  private def drawExitArch(g: Graphics2D): Unit = {
    val x = worldX(PORTAL_X)
    val y = worldY(PORTAL_Y)
    val ready = relicCount == relics.length && aliveEnemyCount == 0
    val tint = if (ready) C_GOLD else C_VIOLET
    val pulse = 1.0 + math.sin(frameCount * 0.045) * 0.055
    val width = (118 * pulse).toInt
    val height = (154 * pulse).toInt
    drawGlow(g, x, y, 116, tint, 0.13f)
    g.setColor(new Color(7, 17, 39, 235))
    g.fillOval(x - width / 2, y - height / 2, width, height)
    g.setStroke(new BasicStroke(6.0f))
    g.setColor(tint)
    g.drawOval(x - width / 2, y - height / 2, width, height)
    g.setStroke(new BasicStroke(2.0f))
    g.setColor(new Color(193, 245, 255, 175))
    g.drawOval(x - width / 2 + 13, y - height / 2 + 14, width - 26, height - 28)
    var i = 0
    while (i < 8) {
      val a = frameCount * 0.022 + i * math.Pi * 2.0 / 8.0
      val px = x + (math.cos(a) * 48).toInt
      val py = y + (math.sin(a) * 61).toInt
      g.setColor(if (i % 2 == 0) C_MINT else C_PINK)
      g.fillOval(px - 4, py - 4, 8, 8)
      i += 1
    }
    g.setFont(new Font("Dialog", Font.BOLD, 10))
    g.setColor(tint)
    drawCentered(g, if (ready) "EXIT OPEN" else "EXIT SEALED", x, y + height / 2 + 20)
  }

  private def drawPlayer(g: Graphics2D): Unit = {
    val x = worldX(playerX)
    val y = worldY(playerY)
    val bob = math.sin(frameCount * 0.18) * 2.0
    if (invulnerability > 0.0 && (frameCount / 5) % 2 == 0) return
    drawGlow(g, x, y, 44, C_MINT, 0.13f)
    g.setColor(new Color(0, 0, 0, 90))
    g.fillOval(x - 19, y + 15, 38, 11)
    g.setColor(new Color(34, 55, 91))
    g.fillRoundRect(x - 14, y + bob.toInt, 11, 21, 6, 6)
    g.fillRoundRect(x + 3, y - bob.toInt, 11, 21, 6, 6)
    g.setColor(C_GOLD)
    g.fillRoundRect(x - 15, y + 16, 13, 6, 3, 3)
    g.fillRoundRect(x + 2, y + 16, 13, 6, 3, 3)
    g.setColor(new Color(37, 91, 128))
    g.fillRoundRect(x - 19, y - 27 + bob.toInt, 38, 39, 12, 12)
    g.setColor(new Color(94, 218, 194))
    g.fillRoundRect(x - 14, y - 22 + bob.toInt, 28, 28, 9, 9)
    g.setColor(new Color(241, 207, 177))
    g.fillOval(x - 12, y - 50 + bob.toInt, 24, 25)
    g.setColor(new Color(37, 30, 54))
    g.fillArc(x - 13, y - 57 + bob.toInt, 26, 25, 0, 180)
    g.setColor(C_WHITE)
    g.fillOval(x - 5, y - 40 + bob.toInt, 3, 3)
    g.fillOval(x + 3, y - 40 + bob.toInt, 3, 3)
    g.setFont(new Font("Dialog", Font.BOLD, 10))
    g.setColor(C_WHITE)
    drawCentered(g, playerName, x, y - 66)
  }

  private def drawRemotePlayers(g: Graphics2D): Unit = {
    networkPlayers.foreach { peer =>
      if (peer.id != localNetworkId) {
        val x = worldX(peer.x)
        val y = worldY(peer.y)
        if (x > -40 && y > -70 && x < 1260 && y < 820) {
          drawGlow(g, x, y, 36, C_BLUE, 0.12f)
          g.setColor(new Color(40, 91, 150))
          g.fillRoundRect(x - 14, y - 25, 28, 40, 10, 10)
          g.setColor(C_BLUE)
          g.fillRoundRect(x - 10, y - 21, 20, 27, 8, 8)
          g.setColor(C_WHITE)
          g.fillOval(x - 9, y - 47, 18, 19)
          g.setFont(new Font("Dialog", Font.BOLD, 10))
          g.setColor(C_GOLD)
          drawCentered(g, peer.name, x, y - 56)
        }
      }
    }
  }

  private def drawGauntlet(g: Graphics2D): Unit = {
    val x = worldX(playerX) + 17
    val y = worldY(playerY) + 2
    g.setColor(new Color(179, 136, 69))
    g.fillRoundRect(x - 2, y - 3, 11, 17, 4, 4)
    currentGem.foreach { gem =>
      drawGlow(g, x + 4, y + 2, 13, gem.tint, 0.24f)
      g.setColor(gem.tint)
      g.fillOval(x + 1, y - 1, 7, 7)
      g.setColor(C_WHITE)
      g.drawOval(x + 1, y - 1, 7, 7)
    }
  }

  private def drawSparks(g: Graphics2D): Unit = {
    sparks.foreach { p =>
      val x = worldX(p.x)
      val y = worldY(p.y)
      if (x >= -10 && y >= -10 && x < 1930 && y < 1350) {
        val alpha = clamp(p.life / 4.0, 0.0, 0.86).toFloat
        val old = g.getComposite
        g.setComposite(AlphaComposite.SrcOver.derive(alpha))
        g.setColor(p.tint)
        val size = math.max(2, p.size.toInt)
        g.fillOval(x - size / 2, y - size / 2, size, size)
        g.setColor(C_WHITE)
        g.fillOval(x - 1, y - 1, 2, 2)
        g.setComposite(old)
      }
    }
  }

  private def drawMiniMap(g: Graphics2D, w: Int): Unit = {
    val x = w - 193
    val y = 106
    val mw = 172
    val mh = 118
    g.setColor(new Color(4, 12, 28, 225))
    g.fillRoundRect(x, y, mw, mh, 14, 14)
    g.setColor(new Color(98, 211, 209, 175))
    g.drawRoundRect(x, y, mw, mh, 14, 14)
    def mx(v: Double): Int = x + 8 + (v / WORLD_W * (mw - 16)).toInt
    def my(v: Double): Int = y + 8 + (v / WORLD_H * (mh - 16)).toInt
    relics.filterNot(_.collected).foreach { r =>
      g.setColor(r.tint)
      g.fillOval(mx(r.x) - 3, my(r.y) - 3, 6, 6)
    }
    g.setColor(C_GOLD)
    g.fillOval(mx(PORTAL_X) - 4, my(PORTAL_Y) - 4, 8, 8)
    g.setColor(C_WHITE)
    g.fillOval(mx(playerX) - 3, my(playerY) - 3, 6, 6)
    g.setFont(new Font("Dialog", Font.BOLD, 9))
    g.drawString("FIELD SCAN", x + 9, y + 13)
  }

  private def drawHUD(g: Graphics2D, w: Int, h: Int): Unit = {
    g.setColor(new Color(4, 13, 31, 225))
    g.fillRoundRect(14, 13, 505, 119, 17, 17)
    g.setColor(new Color(89, 208, 199, 170))
    g.drawRoundRect(14, 13, 505, 119, 17, 17)
    g.setFont(new Font("Dialog", Font.BOLD, 19))
    g.setColor(C_MINT)
    g.drawString("SANCTUS  /  SPRING SEASON 04", 29, 41)
    g.setFont(new Font("Dialog", Font.BOLD, 11))
    g.setColor(C_WHITE)
    g.drawString("REALM " + activeRealm + "  ?  " + currentRealm.name, 29, 63)
    g.setColor(C_GOLD)
    g.setFont(new Font("Dialog", Font.PLAIN, 10))
    g.drawString("SIGILS  " + relicCount + " / " + relics.length + "    ENEMIES  " + aliveEnemyCount + "    KOs  " + enemyKills, 29, 84)
    g.setColor(new Color(35, 57, 77))
    g.fillRoundRect(29, 96, 215, 9, 8, 8)
    g.setColor(C_MINT)
    g.fillRoundRect(29, 96, (215.0 * relicCount / math.max(1, relics.length)).toInt, 9, 8, 8)
    g.setColor(C_PINK)
    g.drawString("CHARGE  " + ("? " * charge), 276, 103)
    g.setColor(C_SOFT)
    g.setFont(new Font("Dialog", Font.PLAIN, 10))
    g.drawString(selectedDifficulty + "  /  " + selectedMode + "  /  " + multiplayerMode, 276, 119)
    g.setColor(new Color(35, 57, 77))
    g.fillRoundRect(29, 111, 215, 7, 7, 7)
    g.setColor(C_PINK)
    g.fillRoundRect(29, 111, (215.0 * playerHealth / 100.0).toInt, 7, 7, 7)
    g.setFont(new Font("Dialog", Font.BOLD, 8))
    g.setColor(C_WHITE)
    g.drawString("HP " + playerHealth + "/100", 250, 117)

    g.setColor(new Color(4, 13, 31, 225))
    g.fillRoundRect(w - 178, 16, 158, 67, 14, 14)
    g.setFont(new Font("Monospaced", Font.BOLD, 22))
    g.setColor(if (secondsLeft < 45.0) C_PINK else C_GOLD)
    val mm = (secondsLeft.toInt / 60) % 100
    val ss = secondsLeft.toInt % 60
    g.drawString(if (selectedMode == "FREE ROAM") "OPEN" else "%02d:%02d".format(mm, ss), w - 157, 48)
    g.setFont(new Font("Dialog", Font.PLAIN, 9))
    g.setColor(C_WHITE)
    g.drawString("TIME WINDOW", w - 155, 68)

    g.setColor(new Color(4, 13, 31, 225))
    g.fillRoundRect(15, h - 49, w - 30, 35, 12, 12)
    g.setFont(new Font("Dialog", Font.BOLD, 10))
    g.setColor(C_MINT)
    g.drawString("WASD / ARROWS  MOVE", 25, h - 27)
    g.setColor(C_GOLD)
    g.drawString("E COLLECT", 185, h - 27)
    g.setColor(C_PINK)
    g.drawString("F FIGHT", 270, h - 27)
    g.setColor(C_BLUE)
    g.drawString("R GEM POWER", 345, h - 27)
    g.setColor(C_MINT)
    g.drawString("1-5 EQUIP  ?  C COLLECTION", 455, h - 27)
    g.setColor(C_WHITE)
    g.drawString("SHIFT DASH  ?  P PAUSE  ?  T RETRY  ?  M REALMS  ?  O ADMIN", 690, h - 27)

    g.setColor(new Color(4, 13, 31, 220))
    g.fillRoundRect(15, 141, 355, 49, 12, 12)
    g.setFont(new Font("Dialog", Font.BOLD, 10))
    g.setColor(C_GOLD)
    g.drawString("ASTRAL GAUNTLET", 27, 159)
    g.setColor(C_WHITE)
    g.drawString("GEMS " + collectedGems.size + "/5", 27, 178)
    currentGem match {
      case Some(gem) =>
        g.setColor(gem.tint)
        g.drawString(gem.name + "  ?  " + gem.powerName, 112, 159)
        g.setColor(C_SOFT)
        g.drawString(if (powerCooldown <= 0.0) "R: READY" else "R: " + math.ceil(powerCooldown).toInt + "s", 112, 178)
      case None =>
        g.setColor(C_SOFT)
        g.drawString("NO GEM EQUIPPED  ?  COLLECT RUBY FIRST", 112, 168)
    }
    if (attackFlash > 0.0) {
      val ax = worldX(playerX + facingX * 54.0)
      val ay = worldY(playerY + facingY * 54.0)
      drawGlow(g, ax, ay, 46, C_GOLD, (attackFlash / 0.20 * 0.24).toFloat)
      g.setColor(C_GOLD)
      g.setStroke(new BasicStroke(4.0f))
      g.drawOval(ax - 28, ay - 28, 56, 56)
    }

    if (pulseFlash > 0.0) {
      val old = g.getComposite
      g.setComposite(AlphaComposite.SrcOver.derive((pulseFlash / 0.55 * 0.20).toFloat))
      g.setColor(C_BLUE)
      g.fillRect(0, 0, w, h)
      g.setComposite(old)
    }

    if (toastTime > 0.0) {
      val width = math.min(w - 40, math.max(330, g.getFontMetrics.stringWidth(toast) + 38))
      val tx = (w - width) / 2
      g.setColor(new Color(4, 15, 34, 235))
      g.fillRoundRect(tx, h - 96, width, 32, 14, 14)
      g.setColor(C_MINT)
      g.drawRoundRect(tx, h - 96, width, 32, 14, 14)
      g.setFont(new Font("Dialog", Font.BOLD, 11))
      g.setColor(C_WHITE)
      drawCentered(g, toast, w / 2, h - 76)
    }

    if (paused || cleared || failed) drawOverlay(g, w, h)
  }

  private def drawOverlay(g: Graphics2D, w: Int, h: Int): Unit = {
    g.setColor(new Color(3, 8, 23, 215))
    g.fillRect(0, 0, w, h)
    val title = if (cleared) "REALM STABILIZED" else if (failed) "SIGNAL LOST" else "FIELD PAUSED"
    val subtitle = if (cleared) "The exit arch has accepted your sigils." else if (failed) "Restore your charge and make another attempt." else "Your expedition is safely paused."
    g.setFont(new Font("Dialog", Font.BOLD, 37))
    g.setColor(if (cleared) C_GOLD else C_WHITE)
    drawCentered(g, title, w / 2, h / 2 - 35)
    g.setFont(new Font("Dialog", Font.PLAIN, 15))
    g.setColor(C_SOFT)
    drawCentered(g, subtitle, w / 2, h / 2 + 2)
    g.setFont(new Font("Dialog", Font.BOLD, 12))
    g.setColor(C_MINT)
    val instruction = if (cleared && activeRealm < realms.length) "ENTER  OPEN NEXT REALM     ?     M  RETURN TO REALMS" else "T  RETRY     ?     M  RETURN TO REALMS     ?     P  RESUME"
    drawCentered(g, instruction, w / 2, h / 2 + 43)
  }

  // ---------------------------------------------------------------------------
  // POWER GEM COLLECTION
  // ---------------------------------------------------------------------------
  private def buildCollectionScreen(): JPanel = {
    val p = verticalPanel()
    addCentered(p, makeTitle("ASTRAL GAUNTLET COLLECTION", 28, C_GOLD), 4)
    addCentered(p, makeLabel("Collect each core from defeated enemies. Equip one core at a time.", 12), 5)
    collectionButtons.clear()
    POWER_GEMS.foreach { gem =>
      addCentered(p, makeTitle(gem.name + "  ?  " + gem.powerName, 15, gem.tint), 16)
      addCentered(p, makeLabel(gem.description, 11, C_SOFT), 2)
      val b = makeButton("LOCKED  ?  DEFEAT ENEMIES TO FIND THIS CORE")
      collectionButtons += b
      addCentered(p, b, 5)
      addAction(b) { equipGem(gem.id); refreshCollection() }
    }
    collectionStatusArea = new JTextArea(3, 48)
    collectionStatusArea.setEditable(false)
    collectionStatusArea.setFocusable(false)
    collectionStatusArea.setBackground(C_PANEL)
    collectionStatusArea.setForeground(C_MINT)
    collectionStatusArea.setFont(new Font("Monospaced", Font.BOLD, 12))
    collectionStatusArea.setBorder(BorderFactory.createLineBorder(C_LINE))
    collectionStatusArea.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(12)); p.add(collectionStatusArea)
    val back = makeButton("RETURN")
    addCentered(p, back, 16)
    addAction(back) { returnFromCollection() }
    p
  }

  private def refreshCollection(): Unit = {
    var i = 0
    while (i < collectionButtons.length && i < POWER_GEMS.length) {
      val gem = POWER_GEMS(i)
      val owned = collectedGems.contains(gem.id)
      collectionButtons(i).setEnabled(owned)
      collectionButtons(i).setText(if (!owned) "LOCKED  ?  DEFEAT AN ENEMY" else if (equippedGem == gem.id) "EQUIPPED  ?  ACTIVE POWER" else "EQUIP THIS CORE")
      i += 1
    }
    if (collectionStatusArea != null) {
      val active = currentGem.map(_.name).getOrElse("NONE")
      collectionStatusArea.setText("COLLECTED: " + collectedGems.size + " / 5\nEQUIPPED: " + active + "\nIN GAME: PRESS 1-5 TO EQUIP  ?  PRESS R TO RELEASE POWER")
    }
  }

  // ---------------------------------------------------------------------------
  // GAME CANVAS AND CONTROLS
  // ---------------------------------------------------------------------------
  private final class RealmCanvas extends JPanel {
    setFocusable(true)
    setBackground(C_BG)
    setPreferredSize(new Dimension(1220, 760))
    setDoubleBuffered(true)

    addKeyListener(new KeyAdapter {
      override def keyPressed(e: KeyEvent): Unit = {
        val code = e.getKeyCode
        val fresh = !keys.contains(code)
        keys += code
        if (!fresh) return
        if (code == KeyEvent.VK_E) interact()
        else if (code == KeyEvent.VK_F) fight()
        else if (code == KeyEvent.VK_R) useEquippedPower()
        else if (code == KeyEvent.VK_C) showCollection(true)
        else if (code >= KeyEvent.VK_1 && code <= KeyEvent.VK_5) equipGem(code - KeyEvent.VK_1)
        else if (code == KeyEvent.VK_SPACE) doPulse()
        else if (code == KeyEvent.VK_SHIFT && dashCooldown <= 0.0 && started && !paused && !cleared && !failed) {
          dashTime = 0.24
          dashCooldown = 1.15
          burst(playerX, playerY, C_MINT, 9)
        }
        else if (code == KeyEvent.VK_P || code == KeyEvent.VK_ESCAPE) {
          if (!cleared && !failed) paused = !paused
        }
        else if (code == KeyEvent.VK_T) restartRealm()
        else if (code == KeyEvent.VK_O && isAdmin) openAdminPanel(true)
        else if (code == KeyEvent.VK_M) returnToRealms()
        else if (code == KeyEvent.VK_ENTER && cleared) {
          if (activeRealm < realms.length) startRealm(activeRealm + 1)
          else returnToRealms()
        }
      }
      override def keyReleased(e: KeyEvent): Unit = keys -= e.getKeyCode
    })

    addMouseListener(new java.awt.event.MouseAdapter {
      override def mousePressed(e: java.awt.event.MouseEvent): Unit = requestFocusInWindow()
    })

    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)
        val w = getWidth
        val h = getHeight
        cameraX = clamp(playerX - w / 2.0, 0.0, math.max(0.0, WORLD_W - w))
        cameraY = clamp(playerY - h / 2.0, 0.0, math.max(0.0, WORLD_H - h))
        drawBackdrop(g, w, h)
        drawTerrain(g, w, h)
        drawLandmark(g)
        drawBlooms(g)
        relics.foreach(r => drawRelic(g, r))
        gemDrops.foreach(d => drawGemDrop(g, d))
        sentries.foreach(s => drawSentry(g, s))
        drawExitArch(g)
        drawSparks(g)
        drawRemotePlayers(g)
        drawPlayer(g)
        drawGauntlet(g)
        drawMiniMap(g, w)
        drawHUD(g, w, h)
      } finally g.dispose()
    }
  }

  // ---------------------------------------------------------------------------
  // SCREEN: LOGIN
  // ---------------------------------------------------------------------------
  private def buildLoginScreen(): JPanel = {
    val p = verticalPanel()
    p.add(Box.createVerticalGlue())
    addCentered(p, makeTitle("SANCTUS", 23, C_MINT), 5)
    addCentered(p, makeTitle("SPRING SEASON  /  FOURTH EVENT", 31, C_WHITE), 4)
    addCentered(p, makeTitle("THE LUMEN CIRCUIT", 17, C_GOLD), 3)
    addCentered(p, makeLabel("Create a field identity to enter the floating realms.", 14), 17)

    val form = new JPanel(new java.awt.GridBagLayout())
    form.setOpaque(false)
    val c = new java.awt.GridBagConstraints()
    c.insets = new java.awt.Insets(8, 8, 8, 8)
    c.fill = java.awt.GridBagConstraints.HORIZONTAL
    c.gridx = 0; c.gridy = 0
    val n = new JLabel("FIELD NAME")
    n.setForeground(C_WHITE)
    n.setFont(new Font("Dialog", Font.BOLD, 12))
    form.add(n, c)
    c.gridx = 1
    loginNameField = new JTextField("PLAYER", 20)
    loginNameField.setFont(new Font("Dialog", Font.PLAIN, 14))
    form.add(loginNameField, c)
    c.gridx = 0; c.gridy = 1
    val pin = new JLabel("ACCESS KEY")
    pin.setForeground(C_WHITE)
    pin.setFont(new Font("Dialog", Font.BOLD, 12))
    form.add(pin, c)
    c.gridx = 1
    loginPinField = new JPasswordField(20)
    form.add(loginPinField, c)
    p.add(Box.createVerticalStrut(8))
    form.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(form)

    val enter = makeButton("ENTER THE CIRCUIT")
    addCentered(p, enter, 16)
    addCentered(p, makeLabel("Authorized control access is checked privately.", 11, new Color(126, 151, 178)), 8)
    p.add(Box.createVerticalGlue())

    addAction(enter) {
      val typed = Option(loginNameField.getText).getOrElse("").trim.replaceAll("[,;|\\r\\n]", "_").take(18)
      val safe = if (typed.isEmpty) "PLAYER" else typed
      val pinText = new String(loginPinField.getPassword)
      isAdmin = safe.equalsIgnoreCase(ADMIN_NAME) && pinText == ADMIN_PIN
      playerName = safe
      loginPinField.setText("")
      updateWelcome()
      if (adminMenuButton != null) adminMenuButton.setVisible(isAdmin)
      showCard("menu")
    }
    p
  }

  // ---------------------------------------------------------------------------
  // SCREEN: MAIN HUB
  // ---------------------------------------------------------------------------
  private def buildMenuScreen(): JPanel = {
    val p = verticalPanel()
    p.add(Box.createVerticalGlue())
    addCentered(p, makeTitle("SANCTUS EVENT", 35, C_MINT), 2)
    addCentered(p, makeTitle("SPRING SEASON  /  FOURTH EVENT", 16, C_GOLD), 3)
    welcomeLabel = makeLabel("Welcome, PLAYER", 14, C_SOFT)
    addCentered(p, welcomeLabel, 18)
    addCentered(p, makeLabel("Fight enemies. Claim five cores. Master one power at a time.", 13, C_BLUE), 5)

    val realmsButton = makeButton("OPEN THE REALM GATE")
    val setupButton = makeButton("EXPEDITION SETTINGS")
    val collectionButton = makeButton("POWER GEM COLLECTION")
    val recordsButton = makeButton("FIELD RECORDS")
    hostButton = makeButton("HOST LAN MULTIPLAYER")
    joinButton = makeButton("JOIN LAN MULTIPLAYER")
    adminMenuButton = makeButton("CONTROL SANCTUM")
    val logoutButton = makeButton("LEAVE FIELD")
    addCentered(p, realmsButton, 14)
    addCentered(p, setupButton, 7)
    addCentered(p, collectionButton, 7)
    addCentered(p, recordsButton, 7)
    addCentered(p, hostButton, 7)
    addCentered(p, joinButton, 7)
    addCentered(p, adminMenuButton, 7)
    addCentered(p, logoutButton, 7)
    adminMenuButton.setVisible(false)
    p.add(Box.createVerticalGlue())

    addAction(realmsButton) { refreshRealmButtons(); showCard("realms") }
    addAction(setupButton) { showCard("settings") }
    addAction(collectionButton) { showCollection(false) }
    addAction(recordsButton) { showRecords() }
    addAction(hostButton) { hostLanGame() }
    addAction(joinButton) { joinLanGame() }
    addAction(adminMenuButton) { openAdminPanel(false) }
    addAction(logoutButton) {
      stopGame()
      closeNetwork()
      playerName = "PLAYER"
      isAdmin = false
      loginNameField.setText("PLAYER")
      loginPinField.setText("")
      adminMenuButton.setVisible(false)
      showCard("login")
    }
    p
  }

  private def updateWelcome(): Unit = {
    if (welcomeLabel != null) welcomeLabel.setText("Welcome, " + playerName + (if (isAdmin) "  ?  CONTROLLER" else "  ?  EXPLORER"))
  }

  // ---------------------------------------------------------------------------
  // SCREEN: REALM SELECT
  // ---------------------------------------------------------------------------
  private def buildRealmsScreen(): JPanel = {
    val p = verticalPanel()
    addCentered(p, makeTitle("CHOOSE A REALM", 29, C_MINT), 3)
    addCentered(p, makeLabel("Each cleared realm opens the next gate.", 13), 4)
    realmButtons.clear()
    var i = 0
    while (i < realms.length) {
      val index = i + 1
      addCentered(p, makeTitle("0" + index + "  /  " + realms(i).name, 17, if (i % 2 == 0) C_GOLD else C_BLUE), 16)
      addCentered(p, makeLabel(realms(i).caption, 12, C_SOFT), 3)
      val b = makeButton("ENTER REALM " + index)
      realmButtons += b
      addCentered(p, b, 6)
      addCentered(p, makeLabel("SIGILS REQUIRED: " + realms(i).relics + "     ?     SENTRIES: " + realms(i).sentries, 10, new Color(136, 163, 189)), 2)
      i += 1
    }
    val back = makeButton("RETURN TO HUB")
    addCentered(p, back, 18)
    p.add(Box.createVerticalGlue())
    i = 0
    while (i < realmButtons.length) {
      val index = i + 1
      val b = realmButtons(i)
      addAction(b) { startRealm(index) }
      i += 1
    }
    addAction(back) { showCard("menu") }
    refreshRealmButtons()
    p
  }

  private def refreshRealmButtons(): Unit = {
    var i = 0
    while (i < realmButtons.length) {
      val number = i + 1
      val complete = completedRealms.contains(number)
      val open = number == 1 || completedRealms.contains(number - 1)
      realmButtons(i).setEnabled(open)
      realmButtons(i).setText(if (complete) "REVISIT REALM " + number + "  ?  STABILIZED" else if (open) "ENTER REALM " + number else "REALM " + number + "  ?  SEALED")
      i += 1
    }
  }

  // ---------------------------------------------------------------------------
  // SCREEN: SETTINGS
  // ---------------------------------------------------------------------------
  private def buildSettingsScreen(): JPanel = {
    val p = verticalPanel()
    p.add(Box.createVerticalGlue())
    addCentered(p, makeTitle("EXPEDITION SETTINGS", 29, C_MINT), 4)
    addCentered(p, makeLabel("Tune the field before opening a realm.", 13), 8)

    val dRow = new JPanel()
    dRow.setOpaque(false)
    val dLabel = new JLabel("FIELD RATING   ")
    dLabel.setForeground(C_WHITE)
    dLabel.setFont(new Font("Dialog", Font.BOLD, 12))
    val difficultyBox = makeCombo(DIFFICULTIES, selectedDifficulty)
    dRow.add(dLabel); dRow.add(difficultyBox)
    dRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(18)); p.add(dRow)

    val mRow = new JPanel()
    mRow.setOpaque(false)
    val mLabel = new JLabel("RUN FORMAT   ")
    mLabel.setForeground(C_WHITE)
    mLabel.setFont(new Font("Dialog", Font.BOLD, 12))
    val modeBox = makeCombo(GAME_MODES, selectedMode)
    mRow.add(mLabel); mRow.add(modeBox)
    mRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(12)); p.add(mRow)

    val descriptions = new JTextArea("EASY       - first enemy is gentler; longer timer\nMEDIUM     - stronger enemies and more health\nHARD       - faster enemies and higher damage\nEXTREME    - large enemy health and tighter timer\nNIGHTMARE  - maximum pressure\n\nADVENTURE  - normal realm progress\nTIME ATTACK - shorter clock\nGEM HUNT   - extra sigils and enemy drops\nFREE ROAM  - no timer or enemies\n\nF: FIGHT   R: GEM POWER   C: COLLECTION   1-5: EQUIP")
    descriptions.setEditable(false)
    descriptions.setFocusable(false)
    descriptions.setFont(new Font("Monospaced", Font.PLAIN, 12))
    descriptions.setForeground(C_SOFT)
    descriptions.setBackground(C_PANEL)
    descriptions.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(C_LINE), BorderFactory.createEmptyBorder(12, 15, 12, 15)))
    descriptions.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(20)); p.add(descriptions)

    val save = makeButton("APPLY SETTINGS")
    val back = makeButton("RETURN TO HUB")
    addCentered(p, save, 18)
    addCentered(p, back, 8)
    p.add(Box.createVerticalGlue())
    addAction(save) {
      selectedDifficulty = difficultyBox.getSelectedItem.toString
      selectedMode = modeBox.getSelectedItem.toString
      JOptionPane.showMessageDialog(frame, "Expedition settings applied.", "Settings", JOptionPane.INFORMATION_MESSAGE)
    }
    addAction(back) { showCard("menu") }
    p
  }

  // ---------------------------------------------------------------------------
  // SCREEN: CONTROL SANCTUM
  // ---------------------------------------------------------------------------
  private def buildAdminScreen(): JPanel = {
    val p = verticalPanel()
    addCentered(p, makeTitle("CONTROL SANCTUM", 29, C_GOLD), 4)
    addCentered(p, makeLabel("Press O in a realm to open this panel at any time; select a player or ALL PLAYERS.", 12), 4)

    val titleRow = new JPanel(); titleRow.setOpaque(false)
    val titleLabel = new JLabel("EVENT TITLE"); titleLabel.setForeground(C_WHITE)
    adminTitleField = new JTextField(eventTitle, 24)
    titleRow.add(titleLabel); titleRow.add(adminTitleField)
    titleRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(12)); p.add(titleRow)

    val subRow = new JPanel(); subRow.setOpaque(false)
    val subLabel = new JLabel("EVENT LINE"); subLabel.setForeground(C_WHITE)
    adminSubtitleField = new JTextField(eventSubtitle, 24)
    subRow.add(subLabel); subRow.add(adminSubtitleField)
    subRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(8)); p.add(subRow)

    val settingRow = new JPanel(); settingRow.setOpaque(false)
    adminDifficultyBox = makeCombo(DIFFICULTIES, selectedDifficulty)
    adminModeBox = makeCombo(GAME_MODES, selectedMode)
    settingRow.add(new JLabel("DIFFICULTY")); settingRow.add(adminDifficultyBox)
    settingRow.add(new JLabel("MODE")); settingRow.add(adminModeBox)
    settingRow.getComponents.foreach { case l: JLabel => l.setForeground(C_WHITE); l.setFont(new Font("Dialog", Font.BOLD, 11)); case _ => () }
    settingRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(8)); p.add(settingRow)

    val targetRow = new JPanel(); targetRow.setOpaque(false)
    val targetLabel = new JLabel("TARGET PLAYER"); targetLabel.setForeground(C_WHITE)
    adminTargetBox = new JComboBox[String]()
    adminTargetBox.setPreferredSize(new Dimension(310, 28))
    targetRow.add(targetLabel); targetRow.add(adminTargetBox)
    targetRow.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(14)); p.add(targetRow)

    val commandPanel = new JPanel(new java.awt.GridLayout(2, 3, 8, 8))
    commandPanel.setOpaque(false)
    val heal = makeButton("HEAL")
    val gems = makeButton("GRANT 5 GEMS")
    val boost = makeButton("SPEED BOOST")
    val freeze = makeButton("FREEZE ENEMIES")
    val clear = makeButton("CLEAR ENEMIES")
    val revive = makeButton("REVIVE")
    commandPanel.add(heal); commandPanel.add(gems); commandPanel.add(boost)
    commandPanel.add(freeze); commandPanel.add(clear); commandPanel.add(revive)
    commandPanel.setMaximumSize(new Dimension(720, 104))
    commandPanel.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(10)); p.add(commandPanel)
    addAction(heal) { sendAdminAction("HEAL") }
    addAction(gems) { sendAdminAction("GEMS") }
    addAction(boost) { sendAdminAction("BOOST") }
    addAction(freeze) { sendAdminAction("FREEZE") }
    addAction(clear) { sendAdminAction("CLEAR") }
    addAction(revive) { sendAdminAction("REVIVE") }

    adminStatusArea = new JTextArea(10, 55)
    adminStatusArea.setEditable(false)
    adminStatusArea.setFont(new Font("Monospaced", Font.PLAIN, 12))
    adminStatusArea.setForeground(C_MINT)
    adminStatusArea.setBackground(new Color(4, 13, 30))
    adminStatusArea.setBorder(BorderFactory.createLineBorder(C_LINE))
    val scroller = new JScrollPane(adminStatusArea)
    scroller.setAlignmentX(Component.CENTER_ALIGNMENT)
    p.add(Box.createVerticalStrut(12)); p.add(scroller)

    val save = makeButton("SAVE EVENT CONFIGURATION")
    val refresh = makeButton("REFRESH PLAYER LIST")
    val back = makeButton("RETURN TO HUB")
    addCentered(p, save, 12); addCentered(p, refresh, 7); addCentered(p, back, 7)
    p.add(Box.createVerticalGlue())
    addAction(save) {
      val t = Option(adminTitleField.getText).getOrElse("").trim
      val sub = Option(adminSubtitleField.getText).getOrElse("").trim
      if (t.nonEmpty) eventTitle = t.take(48)
      if (sub.nonEmpty) eventSubtitle = sub.take(64)
      selectedDifficulty = adminDifficultyBox.getSelectedItem.toString
      selectedMode = adminModeBox.getSelectedItem.toString
      if (started) {
        // Keep the host and LAN clients on the same realm configuration.
        buildRealm()
        if (gameCanvas != null) gameCanvas.requestFocusInWindow()
      }
      refreshAdmin()
      JOptionPane.showMessageDialog(frame, "Configuration saved for this session." + (if (started) " The current realm has been reset to apply the new settings." else ""), "Control Sanctum", JOptionPane.INFORMATION_MESSAGE)
    }
    addAction(refresh) { refreshAdmin() }
    addAction(back) { closeAdminPanel() }
    refreshAdmin()
    p
  }

  private def refreshAdminTargets(): Unit = {
    if (adminTargetBox != null) {
      val desired = Vector("ALL PLAYERS") ++ networkPlayers.map(peer => peer.id + "  ?  " + peer.name)
      val current = (0 until adminTargetBox.getItemCount).map(i => adminTargetBox.getItemAt(i)).toVector
      // Do not rebuild the combo while its popup is open unless the roster changed.
      if (current != desired) {
        val old = Option(adminTargetBox.getSelectedItem).map(_.toString).getOrElse("ALL PLAYERS")
        adminTargetBox.removeAllItems()
        desired.foreach(item => adminTargetBox.addItem(item))
        val idx = (0 until adminTargetBox.getItemCount).find(i => adminTargetBox.getItemAt(i) == old).getOrElse(0)
        adminTargetBox.setSelectedIndex(idx)
      }
    }
  }

  private def refreshAdmin(): Unit = {
    if (adminTitleField != null) adminTitleField.setText(eventTitle)
    if (adminSubtitleField != null) adminSubtitleField.setText(eventSubtitle)
    if (adminDifficultyBox != null) adminDifficultyBox.setSelectedItem(selectedDifficulty)
    if (adminModeBox != null) adminModeBox.setSelectedItem(selectedMode)
    refreshAdminTargets()
    if (adminStatusArea != null) {
      val rows = ArrayBuffer[String]()
      rows += "SPRING SEASON  /  FOURTH EVENT"
      rows += "EVENT TITLE : " + eventTitle
      rows += "CONTROLLER  : " + (if (isAdmin) ADMIN_NAME else "PLAYER")
      rows += "RATING      : " + selectedDifficulty
      rows += "FORMAT      : " + selectedMode
      rows += "NETWORK     : " + multiplayerMode
      rows += "CONNECTED   : " + networkPlayers.length + " PLAYER(S)"
      rows += ""
      rows += "CONNECTED PLAYER ROSTER"
      if (networkPlayers.isEmpty) rows += "No LAN players in this session. Host a room or join one from the hub."
      networkPlayers.foreach { peer => rows += (peer.id + "  " + peer.name + "  HP " + peer.hp + "  REALM " + peer.realm) }
      rows += ""
      rows += "ADMIN ACTIONS: HEAL / GEMS / BOOST / FREEZE / CLEAR / REVIVE"
      rows += "Changes remain active until the application closes."
      adminStatusArea.setText(rows.mkString("\n"))
    }
  }

  // ---------------------------------------------------------------------------
  // FIELD RECORDS
  // ---------------------------------------------------------------------------
  private def showRecords(): Unit = {
    val text = new StringBuilder
    text.append("SANCTUS EVENT  /  FIELD RECORDS\n\n")
    text.append("Explorer: ").append(playerName).append("\n")
    text.append("Stabilized realms: ").append(completedRealms.size).append(" / ").append(realms.length).append("\n\n")
    realms.indices.foreach { i =>
      text.append("0").append(i + 1).append("  ").append(realms(i).name).append("  ?  ")
      text.append(if (completedRealms.contains(i + 1)) "STABILIZED" else "NOT YET STABILIZED").append("\n")
    }
    text.append("\nPower gems collected: ").append(collectedGems.size).append(" / 5")
    text.append("\nEnemy defeats this realm: ").append(enemyKills)
    text.append("\nEquipped core: ").append(currentGem.map(_.name).getOrElse("NONE"))
    text.append("\nCurrent rating: ").append(selectedDifficulty)
    text.append("\nCurrent format: ").append(selectedMode)
    JOptionPane.showMessageDialog(frame, text.toString, "Field Records", JOptionPane.INFORMATION_MESSAGE)
  }

  // ---------------------------------------------------------------------------
  // WINDOW INITIALIZATION
  // ---------------------------------------------------------------------------
  private def buildUI(): Unit = {
    cardLayout = new java.awt.CardLayout()
    root = new JPanel(cardLayout)
    root.setBackground(C_BG)
    root.add(scrollCard(buildLoginScreen()), "login")
    root.add(scrollCard(buildMenuScreen()), "menu")
    root.add(scrollCard(buildRealmsScreen()), "realms")
    root.add(scrollCard(buildCollectionScreen()), "collection")
    root.add(scrollCard(buildSettingsScreen()), "settings")
    root.add(scrollCard(buildAdminScreen()), "admin")

    frame = new JFrame(APP_TITLE)
    frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE)
    frame.setContentPane(root)
    frame.addWindowListener(new WindowAdapter {
      override def windowClosing(e: WindowEvent): Unit = {
        stopGame()
        if (networkAdminTimer != null) networkAdminTimer.stop()
        closeNetwork()
        frame.dispose()
      }
    })
    frame.setMinimumSize(new Dimension(900, 650))
    frame.setSize(1180, 790)
    frame.setLocationRelativeTo(null)
    frame.setVisible(true)
    networkAdminTimer = new Timer(120, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        consumeNetworkAdminCommands()
        consumeNetworkActions()
        if (adminStatusArea != null && root != null && cardLayout != null) {
          // Refresh only the target-player list; do not overwrite text being edited.
          if (adminTargetBox != null && adminTargetBox.isShowing) refreshAdminTargets()
        }
      }
    })
    networkAdminTimer.start()
    showCard("login")
  }

  def main(args: Array[String]): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = buildUI()
    })
  }
}


// ===== INTEGRATED GAME MODULE: SpringSeason_CyberBloomGlitchInTheGarden.scala =====

import java.awt.{BasicStroke, Color, Dimension, Font, Graphics, Graphics2D, GradientPaint, Polygon, RenderingHints, Rectangle}
import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, MouseAdapter, MouseEvent, FocusAdapter, FocusEvent}
import java.awt.geom.Ellipse2D
import javax.swing.{JFrame, JPanel, SwingUtilities, Timer, WindowConstants}
import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.HashSet
import scala.util.Random

/**
 * SPRING SEASON: CYBER BLOOM ? GLITCH IN THE GARDEN
 * An original animated Scala + Swing mini-game.
 * Uses procedural shapes; no external assets or extra libraries are required.
 * Run this object from a Scala 2.x environment with Java Swing available.
 */
object SpringSeason_CyberBloomGlitchInTheGarden {
  val VIEW_W = 1200
  val VIEW_H = 760
  val TILE_W = 92.0
  val TILE_H = 46.0
  val WORLD_U = 28
  val WORLD_V = 22
  val ROUND_SECONDS = 180.0

  val BACKGROUND = new Color(7, 12, 27)
  val PANEL = new Color(10, 20, 38, 220)
  val CYAN = new Color(65, 245, 235)
  val PINK = new Color(255, 90, 205)
  val LIME = new Color(160, 255, 105)
  val PURPLE = new Color(166, 100, 255)
  val GOLD = new Color(255, 220, 110)
  val random = new Random(8242026L)

  case class Flower(u: Double, v: Double, hue: Float, var bloomed: Boolean = false, phase: Double = 0.0)
  case class Seed(u: Double, v: Double, var collected: Boolean = false, phase: Double = 0.0)
  case class Tree(u: Double, v: Double, hue: Float, size: Double)
  case class Bug(var u: Double, var v: Double, var phase: Double, speed: Double)
  case class Particle(var x: Double, var y: Double, var vx: Double, var vy: Double,
                      var life: Double, maxLife: Double, color: Color, size: Double)
  case class RenderItem(depth: Double, draw: () => Unit)

  val keys = HashSet[Int]()
  val flowers = ArrayBuffer[Flower]()
  val seeds = ArrayBuffer[Seed]()
  val trees = ArrayBuffer[Tree]()
  val bugs = ArrayBuffer[Bug]()
  val particles = ArrayBuffer[Particle]()

  var window: JFrame = _
  var panel: GardenPanel = _
  var clock: Timer = _
  var state = "title" // title, playing, paused, win, failed
  var playerU = 3.8
  var playerV = 3.9
  var cameraX = 0.0
  var cameraY = 0.0
  var elapsed = 0.0
  var energy = 100.0
  var collectedSeeds = 0
  var bloomedFlowers = 0
  var invulnerable = 0.0
  var dashTimer = 0.0
  var dashCooldown = 0.0
  var animationTime = 0.0
  var glitchTimer = 0.0
  var statusMessage = "Find CYBER SEEDS and awaken the flowers!"
  var statusTimer = 4.0
  var showMap = true
  var pointerX = 0
  var pointerY = 0

  val startButton = new Rectangle(VIEW_W / 2 - 140, VIEW_H - 154, 280, 62)

  def buildWorld(): Unit = {
    flowers.clear(); seeds.clear(); trees.clear(); bugs.clear(); particles.clear()
    val flowerData = Seq(
      (5.0, 4.0, 0.88f), (7.0, 7.0, 0.53f), (10.0, 4.8, 0.77f),
      (12.0, 9.0, 0.10f), (15.0, 5.0, 0.86f), (18.0, 8.0, 0.57f),
      (20.0, 12.0, 0.73f), (24.0, 9.0, 0.93f), (8.0, 13.0, 0.15f),
      (12.0, 16.0, 0.68f), (18.0, 17.0, 0.48f), (24.0, 17.0, 0.82f),
      (4.0, 17.0, 0.58f), (15.0, 12.0, 0.04f), (22.0, 5.0, 0.64f)
    )
    flowerData.foreach { p => flowers += Flower(p._1, p._2, p._3, false, random.nextDouble() * 10.0) }

    val seedData = Seq(
      (4.8, 6.0), (8.5, 4.0), (10.5, 10.0), (14.0, 7.0),
      (17.0, 11.0), (22.0, 8.0), (6.5, 14.5), (11.5, 18.0),
      (17.5, 15.0), (22.0, 16.0), (25.0, 12.5), (14.5, 3.0)
    )
    seedData.foreach { p => seeds += Seed(p._1, p._2, false, random.nextDouble() * 10.0) }

    val treeData = Seq(
      (2.0, 4.0, 0.47f, 1.0), (3.0, 10.0, 0.53f, 1.1), (6.0, 19.0, 0.48f, 1.25),
      (10.0, 2.0, 0.58f, 0.95), (14.0, 20.0, 0.78f, 1.2), (20.0, 3.0, 0.48f, 1.1),
      (26.0, 7.0, 0.58f, 1.2), (26.0, 17.0, 0.83f, 1.25), (3.0, 20.0, 0.73f, 1.15),
      (19.0, 20.0, 0.53f, 1.0), (24.0, 20.0, 0.65f, 1.1), (2.0, 14.0, 0.62f, 0.9)
    )
    treeData.foreach { p => trees += Tree(p._1, p._2, p._3, p._4) }
    bugs += Bug(9.0, 8.0, 0.2, 0.64)
    bugs += Bug(17.0, 10.0, 2.1, 0.72)
    bugs += Bug(22.0, 14.0, 4.0, 0.60)
  }

  def resetGame(): Unit = {
    buildWorld()
    state = "playing"
    playerU = 3.8; playerV = 3.9
    elapsed = 0.0; energy = 100.0
    collectedSeeds = 0; bloomedFlowers = 0
    invulnerable = 0.0; dashTimer = 0.0; dashCooldown = 0.0
    animationTime = 0.0; glitchTimer = 0.0
    statusMessage = "Collect 8 CYBER SEEDS, bloom 6 flowers, then reach the portal."
    statusTimer = 6.0
    keys.clear()
    updateCamera()
  }

  def say(text: String, seconds: Double = 2.6): Unit = {
    statusMessage = text
    statusTimer = seconds
  }

  def projectWorld(u: Double, v: Double): (Double, Double) = {
    val wx = (u - v) * TILE_W / 2.0
    val wy = (u + v) * TILE_H / 2.0
    (wx, wy)
  }

  def updateCamera(): Unit = {
    val p = projectWorld(playerU, playerV)
    cameraX = p._1
    cameraY = p._2
  }

  def toScreen(u: Double, v: Double): (Double, Double) = {
    val p = projectWorld(u, v)
    (VIEW_W / 2.0 + p._1 - cameraX, VIEW_H * 0.55 + p._2 - cameraY)
  }

  def distance(u1: Double, v1: Double, u2: Double, v2: Double): Double = {
    math.sqrt((u1 - u2) * (u1 - u2) + (v1 - v2) * (v1 - v2))
  }

  def spawnBurst(sx: Double, sy: Double, color: Color, amount: Int): Unit = {
    var i = 0
    while (i < amount) {
      val a = random.nextDouble() * math.Pi * 2.0
      val speed = 35.0 + random.nextDouble() * 145.0
      particles += Particle(sx, sy, math.cos(a) * speed, math.sin(a) * speed - 35.0,
        0.45 + random.nextDouble() * 0.65, 1.1, color, 2.0 + random.nextDouble() * 4.0)
      i += 1
    }
  }

  def interact(): Unit = {
    if (state != "playing") return
    val bloom = flowers.filterNot(_.bloomed).sortBy(f => distance(playerU, playerV, f.u, f.v)).headOption
    bloom match {
      case Some(f) if distance(playerU, playerV, f.u, f.v) < 1.45 =>
        f.bloomed = true
        bloomedFlowers += 1
        val p = toScreen(f.u, f.v)
        spawnBurst(p._1, p._2 - 20, Color.getHSBColor(f.hue, 0.75f, 1.0f), 24)
        energy = math.min(100.0, energy + 5.0)
        say("CYBER BLOOM AWAKENED!  " + bloomedFlowers + "/6", 2.3)
      case _ =>
        if (distance(playerU, playerV, 26.0, 19.0) < 2.0) {
          if (collectedSeeds >= 8 && bloomedFlowers >= 6) {
            state = "win"
            say("PORTAL STABLE ? GARDEN RESTORED!", 10.0)
            val p = toScreen(26.0, 19.0)
            spawnBurst(p._1, p._2, CYAN, 90)
          } else {
            say("PORTAL LOCKED: " + collectedSeeds + "/8 seeds, " + bloomedFlowers + "/6 blooms", 3.0)
          }
        } else {
          say("Move close to a sleeping flower and press E to bloom it.", 2.7)
        }
    }
  }

  def keyPressed(k: Int): Unit = {
    keys += k
    if (state == "title" && (k == KeyEvent.VK_ENTER || k == KeyEvent.VK_SPACE)) resetGame()
    else if (k == KeyEvent.VK_ESCAPE) {
      if (state == "playing") state = "paused"
      else if (state == "paused") state = "playing"
      else if (state == "title") ()
    }
    else if (state == "playing") {
      if (k == KeyEvent.VK_E) interact()
      if (k == KeyEvent.VK_TAB) showMap = !showMap
      if (k == KeyEvent.VK_SPACE && dashCooldown <= 0.0) {
        dashTimer = 0.22
        dashCooldown = 1.0
        say("GLITCH DASH!", 0.75)
      }
    }
    else if ((state == "win" || state == "failed") && (k == KeyEvent.VK_R || k == KeyEvent.VK_ENTER)) resetGame()
    else if (state == "paused" && k == KeyEvent.VK_R) resetGame()
    if (panel != null) panel.repaint()
  }

  def tick(): Unit = {
    val dt = 0.016
    animationTime += dt
    if (statusTimer > 0.0) statusTimer -= dt
    if (invulnerable > 0.0) invulnerable -= dt
    if (dashTimer > 0.0) dashTimer -= dt
    if (dashCooldown > 0.0) dashCooldown -= dt
    if (glitchTimer > 0.0) glitchTimer -= dt
    if (random.nextDouble() < 0.006) glitchTimer = 0.10 + random.nextDouble() * 0.08

    if (state == "playing") {
      elapsed += dt
      if (elapsed >= ROUND_SECONDS) {
        state = "failed"
        say("TIME OUT ? press R to try again.", 8.0)
      }

      var du = 0.0
      var dv = 0.0
      if (keys.contains(KeyEvent.VK_W) || keys.contains(KeyEvent.VK_UP)) { du -= 1.0; dv -= 1.0 }
      if (keys.contains(KeyEvent.VK_S) || keys.contains(KeyEvent.VK_DOWN)) { du += 1.0; dv += 1.0 }
      if (keys.contains(KeyEvent.VK_A) || keys.contains(KeyEvent.VK_LEFT)) { du -= 1.0; dv += 1.0 }
      if (keys.contains(KeyEvent.VK_D) || keys.contains(KeyEvent.VK_RIGHT)) { du += 1.0; dv -= 1.0 }
      val len = math.sqrt(du * du + dv * dv)
      if (len > 0.0) {
        val speed = if (dashTimer > 0.0) 7.0 else if (keys.contains(KeyEvent.VK_SHIFT)) 4.0 else 2.45
        playerU = math.max(1.0, math.min(WORLD_U - 1.0, playerU + du / len * speed * dt))
        playerV = math.max(1.0, math.min(WORLD_V - 1.0, playerV + dv / len * speed * dt))
      }
      updateCamera()

      seeds.foreach { s =>
        if (!s.collected && distance(playerU, playerV, s.u, s.v) < 0.62) {
          s.collected = true
          collectedSeeds += 1
          energy = math.min(100.0, energy + 6.0)
          val p = toScreen(s.u, s.v)
          spawnBurst(p._1, p._2, CYAN, 18)
          say("CYBER SEED +1   " + collectedSeeds + "/8", 1.8)
        }
      }

      bugs.foreach { b =>
        b.phase = b.phase + dt * b.speed
        // Floating glitch moths slowly drift around their patrol area.
        b.u += math.sin(b.phase * 0.8) * dt * 0.45
        b.v += math.cos(b.phase * 0.65) * dt * 0.35
        b.u = math.max(2.0, math.min(WORLD_U - 2.0, b.u))
        b.v = math.max(2.0, math.min(WORLD_V - 2.0, b.v))
        if (invulnerable <= 0.0 && distance(playerU, playerV, b.u, b.v) < 0.55) {
          energy = math.max(0.0, energy - 20.0)
          invulnerable = 1.15
          val p = toScreen(playerU, playerV)
          spawnBurst(p._1, p._2 - 24, PINK, 22)
          say("GLITCH MOTH HIT! Energy " + energy.toInt + "%", 2.0)
          if (energy <= 0.0) state = "failed"
        }
      }
      if (collectedSeeds >= 8 && bloomedFlowers >= 6 && statusTimer < 0.4) {
        statusMessage = "PORTAL OPEN ? travel to the glowing gate at the far garden edge and press E."
        statusTimer = 0.4
      }
    }

    particles.foreach { p =>
      p.x += p.vx * dt
      p.y += p.vy * dt
      p.vy += 90.0 * dt
      p.life -= dt
    }
    val alive = particles.filter(_.life > 0.0)
    particles.clear(); particles ++= alive
    if (panel != null) panel.repaint()
  }

  def polygon(g: Graphics2D, xs: Array[Int], ys: Array[Int], c: Color): Unit = {
    g.setColor(c)
    g.fillPolygon(new Polygon(xs, ys, xs.length))
  }

  def drawBackground(g: Graphics2D): Unit = {
    g.setPaint(new GradientPaint(0f, 0f, new Color(6, 14, 35), 0f, VIEW_H.toFloat, new Color(20, 38, 57)))
    g.fillRect(0, 0, VIEW_W, VIEW_H)
    // Distant aurora arcs.
    for (i <- 0 until 6) {
      val y = 75 + i * 24
      g.setColor(new Color(35, 225, 200, 12 + (i % 3) * 5))
      g.setStroke(new BasicStroke(18f + i * 2.0f))
      g.drawArc(-180 + i * 80, y, VIEW_W + 300, 230 + i * 10, 5, 170)
    }
    // Small neon stars.
    for (i <- 0 until 70) {
      val x = (i * 173 + 41) % VIEW_W
      val y = (i * 89 + 17) % 330
      val twinkle = 0.5 + 0.5 * math.sin(animationTime * 2.0 + i)
      g.setColor(new Color(120, 240, 240, (35 + 140 * twinkle).toInt))
      g.fillOval(x, y, if (i % 8 == 0) 3 else 2, if (i % 8 == 0) 3 else 2)
    }
  }

  def drawTile(g: Graphics2D, u: Int, v: Int): Unit = {
    val p = toScreen(u + 0.5, v + 0.5)
    val x = p._1.toInt
    val y = p._2.toInt
    if (x < -TILE_W || x > VIEW_W + TILE_W || y < -TILE_H || y > VIEW_H + TILE_H) return
    val xs = Array(x, x + (TILE_W / 2).toInt, x, x - (TILE_W / 2).toInt)
    val ys = Array(y - (TILE_H / 2).toInt, y, y + (TILE_H / 2).toInt, y)
    val path = (math.abs(u - v) <= 1) || (math.abs(u + v - 25) <= 1)
    val patch = ((u * 7 + v * 11) % 9 == 0)
    val col = if (path) new Color(30, 78, 77) else if (patch) new Color(27, 91, 68) else if ((u + v) % 2 == 0) new Color(18, 65, 53) else new Color(20, 73, 56)
    polygon(g, xs, ys, col)
    g.setColor(if (path) new Color(55, 230, 205, 90) else new Color(82, 173, 117, 45))
    g.setStroke(new BasicStroke(1f))
    g.drawPolygon(xs, ys, 4)
    if (path && (u + v) % 3 == 0) {
      g.setColor(new Color(67, 250, 221, 65))
      g.drawLine(x - 14, y, x + 14, y)
    }
  }

  def drawTree(g: Graphics2D, t: Tree): Unit = {
    val p = toScreen(t.u, t.v)
    val x = p._1.toInt
    val y = p._2.toInt
    if (x < -100 || x > VIEW_W + 100 || y < -150 || y > VIEW_H + 100) return
    val size = t.size
    g.setColor(new Color(4, 18, 24, 115))
    g.fillOval(x - (22 * size).toInt, y - 4, (48 * size).toInt, (16 * size).toInt)
    g.setColor(new Color(54, 61, 51))
    g.fillRoundRect(x - (6 * size).toInt, y - (52 * size).toInt, (12 * size).toInt, (55 * size).toInt, 8, 8)
    val c = Color.getHSBColor(t.hue, 0.74f, 0.82f)
    for (i <- 0 until 4) {
      val ox = ((i % 2) * 22 - 11) * size
      val oy = ((i / 2) * 17 - 58) * size
      g.setColor(new Color(c.getRed, c.getGreen, c.getBlue, 150))
      g.fillOval((x + ox - 25 * size).toInt, (y + oy - 29 * size).toInt, (50 * size).toInt, (48 * size).toInt)
      g.setColor(new Color(120, 255, 229, 90))
      g.drawOval((x + ox - 25 * size).toInt, (y + oy - 29 * size).toInt, (50 * size).toInt, (48 * size).toInt)
    }
    g.setColor(new Color(175, 255, 233, 145))
    g.fillOval(x - 3, y - (65 * size).toInt, 6, 6)
  }

  def drawFlower(g: Graphics2D, f: Flower): Unit = {
    val p = toScreen(f.u, f.v)
    val x = p._1.toInt
    val y = p._2.toInt
    if (x < -50 || x > VIEW_W + 50 || y < -90 || y > VIEW_H + 80) return
    val sway = math.sin(animationTime * 2.0 + f.phase) * 3.0
    val bloomSize = if (f.bloomed) 1.0 + 0.08 * math.sin(animationTime * 4.0 + f.phase) else 0.72
    val petal = Color.getHSBColor(f.hue, if (f.bloomed) 0.72f else 0.35f, if (f.bloomed) 1.0f else 0.54f)
    if (f.bloomed) {
      g.setColor(new Color(65, 245, 220, 32))
      g.fillOval(x - 29, y - 47, 58, 58)
    }
    g.setColor(new Color(60, 215, 145))
    g.setStroke(new BasicStroke(3f))
    g.drawLine(x, y + 3, (x + sway).toInt, y - 21)
    g.fillOval(x - 13, y - 12, 14, 6)
    g.fillOval(x + 1, y - 18, 13, 6)
    val cx = (x + sway).toInt
    val cy = y - 25
    val r = (9.0 * bloomSize).toInt
    for (i <- 0 until 6) {
      val a = i * math.Pi / 3.0 + animationTime * 0.15
      val px = cx + (math.cos(a) * r * 0.72).toInt
      val py = cy + (math.sin(a) * r * 0.72).toInt
      g.setColor(petal)
      g.fillOval(px - r / 2, py - r / 2, r, r)
      if (f.bloomed) {
        g.setColor(new Color(255, 255, 255, 105))
        g.drawOval(px - r / 2, py - r / 2, r, r)
      }
    }
    g.setColor(if (f.bloomed) GOLD else new Color(135, 164, 106))
    g.fillOval(cx - 4, cy - 4, 8, 8)
    if (!f.bloomed) {
      g.setColor(new Color(210, 255, 215, 170))
      g.drawOval(cx - 13, cy - 13, 26, 26)
    }
  }

  def drawSeed(g: Graphics2D, s: Seed): Unit = {
    if (s.collected) return
    val p = toScreen(s.u, s.v)
    val x = p._1.toInt
    val y = (p._2 - 9 - math.sin(animationTime * 3.0 + s.phase) * 5).toInt
    if (x < -20 || x > VIEW_W + 20 || y < -30 || y > VIEW_H + 30) return
    g.setColor(new Color(45, 248, 240, 42))
    g.fillOval(x - 17, y - 17, 34, 34)
    g.setColor(CYAN)
    g.fillOval(x - 7, y - 10, 14, 20)
    g.setColor(Color.WHITE)
    g.fillOval(x - 2, y - 7, 4, 7)
    g.setColor(new Color(65, 255, 235, 160))
    g.drawOval(x - 10, y - 13, 20, 26)
  }

  def drawBug(g: Graphics2D, b: Bug): Unit = {
    val p = toScreen(b.u, b.v)
    val x = p._1.toInt
    val y = (p._2 - 13 - math.sin(b.phase * 2.0) * 4).toInt
    if (x < -30 || x > VIEW_W + 30 || y < -30 || y > VIEW_H + 30) return
    val wing = (math.sin(animationTime * 19.0 + b.phase) * 4).toInt
    g.setColor(new Color(255, 38, 175, 60))
    g.fillOval(x - 18, y - 13, 36, 26)
    g.setColor(PINK)
    g.fillOval(x - 14, y - 9 - wing, 13, 12 + wing.abs)
    g.fillOval(x + 1, y - 9 + wing, 13, 12 + wing.abs)
    g.setColor(new Color(35, 19, 58))
    g.fillOval(x - 4, y - 6, 8, 13)
    g.setColor(Color.WHITE)
    g.fillOval(x - 2, y - 3, 2, 3)
    g.fillOval(x + 1, y - 3, 2, 3)
    g.setColor(new Color(255, 80, 225, 160))
    g.drawLine(x - 4, y - 5, x - 8, y - 12)
    g.drawLine(x + 4, y - 5, x + 8, y - 12)
  }

  def drawPlayer(g: Graphics2D): Unit = {
    val p = toScreen(playerU, playerV)
    val x = p._1.toInt
    val y = p._2.toInt
    val walking = if (keys.exists(k => k == KeyEvent.VK_W || k == KeyEvent.VK_A || k == KeyEvent.VK_S || k == KeyEvent.VK_D || k == KeyEvent.VK_UP || k == KeyEvent.VK_DOWN || k == KeyEvent.VK_LEFT || k == KeyEvent.VK_RIGHT)) 1.0 else 0.0
    val swing = math.sin(animationTime * 12.0) * 5.0 * walking
    val alpha = if (invulnerable > 0.0 && (animationTime * 17).toInt % 2 == 0) 110 else 255
    g.setColor(new Color(0, 0, 0, 100))
    g.fillOval(x - 18, y - 2, 36, 13)
    g.setColor(new Color(65, 240, 225, 95))
    g.fillOval(x - 18, y - 5, 36, 15)
    g.setColor(new Color(21, 32, 57, alpha))
    g.fillRoundRect(x - 11, y - 34, 10, 27 + swing.toInt, 7, 7)
    g.fillRoundRect(x + 1, y - 34, 10, 27 - swing.toInt, 7, 7)
    g.setColor(new Color(50, 240, 235, alpha))
    g.fillRoundRect(x - 13, y - 54, 26, 28, 10, 10)
    g.setColor(new Color(15, 36, 68, alpha))
    g.fillRoundRect(x - 20, y - 52, 8, 24, 5, 5)
    g.fillRoundRect(x + 12, y - 52, 8, 24, 5, 5)
    g.setColor(new Color(255, 214, 175, alpha))
    g.fillOval(x - 10, y - 77, 20, 22)
    g.setColor(new Color(22, 29, 60, alpha))
    g.fillArc(x - 11, y - 80, 22, 17, 0, 180)
    g.setColor(new Color(24, 38, 64, alpha))
    g.fillRect(x - 11, y - 72, 4, 10)
    g.fillRect(x + 7, y - 72, 4, 10)
    g.setColor(new Color(255, 255, 255, alpha))
    g.fillOval(x - 5, y - 69, 3, 3)
    g.fillOval(x + 2, y - 69, 3, 3)
    if (dashTimer > 0.0) {
      g.setColor(new Color(80, 245, 255, 100))
      g.setStroke(new BasicStroke(3f))
      g.drawOval(x - 24, y - 62, 48, 58)
    }
  }

  def drawPortal(g: Graphics2D): Unit = {
    val p = toScreen(26.0, 19.0)
    val x = p._1.toInt
    val y = p._2.toInt
    val unlocked = collectedSeeds >= 8 && bloomedFlowers >= 6
    val pulse = (Math.sin(animationTime * 3.2) * 5.0).toInt
    val c = if (unlocked) CYAN else PURPLE
    g.setColor(new Color(c.getRed, c.getGreen, c.getBlue, 35))
    g.fillOval(x - 43 - pulse, y - 88 - pulse, 86 + pulse * 2, 105 + pulse * 2)
    g.setColor(new Color(c.getRed, c.getGreen, c.getBlue, 120))
    g.setStroke(new BasicStroke(7f))
    g.drawOval(x - 28, y - 80, 56, 88)
    g.setStroke(new BasicStroke(3f))
    g.setColor(new Color(240, 255, 255, 180))
    g.drawOval(x - 18, y - 71, 36, 70)
    g.setColor(new Color(70, 70, 120, 155))
    g.fillOval(x - 17, y - 68, 34, 64)
    for (i <- 0 until 5) {
      val a = animationTime * 1.4 + i * math.Pi * 2.0 / 5.0
      val sx = x + (Math.cos(a) * 43).toInt
      val sy = y - 42 + (Math.sin(a) * 45).toInt
      g.setColor(c)
      g.fillOval(sx - 3, sy - 3, 6, 6)
    }
    g.setFont(new Font("SansSerif", Font.BOLD, 12))
    g.setColor(if (unlocked) CYAN else new Color(210, 190, 255))
    val label = if (unlocked) "GARDEN PORTAL" else "LOCKED PORTAL"
    g.drawString(label, x - g.getFontMetrics.stringWidth(label) / 2, y + 21)
  }

  def drawParticles(g: Graphics2D): Unit = {
    particles.foreach { p =>
      val alpha = math.max(0, math.min(255, (255.0 * p.life / p.maxLife).toInt))
      g.setColor(new Color(p.color.getRed, p.color.getGreen, p.color.getBlue, alpha))
      val sz = math.max(1, p.size.toInt)
      g.fillOval(p.x.toInt - sz / 2, p.y.toInt - sz / 2, sz, sz)
    }
  }

  def drawHUD(g: Graphics2D): Unit = {
    g.setColor(PANEL)
    g.fillRoundRect(18, 18, 385, 112, 20, 20)
    g.setColor(new Color(65, 245, 235, 130))
    g.setStroke(new BasicStroke(1.4f))
    g.drawRoundRect(18, 18, 385, 112, 20, 20)
    g.setFont(new Font("SansSerif", Font.BOLD, 18))
    g.setColor(CYAN)
    g.drawString("CYBER BLOOM // SPRING SEASON", 34, 45)
    g.setFont(new Font("SansSerif", Font.PLAIN, 13))
    g.setColor(Color.WHITE)
    g.drawString("CYBER SEEDS  " + collectedSeeds + "/8", 34, 70)
    g.drawString("BLOOMS  " + bloomedFlowers + "/6", 180, 70)
    g.drawString("TIME  " + math.max(0, math.ceil(ROUND_SECONDS - elapsed).toInt) + "s", 302, 70)
    g.setColor(new Color(30, 48, 64))
    g.fillRoundRect(34, 84, 220, 13, 7, 7)
    g.setColor(if (energy > 40) LIME else PINK)
    g.fillRoundRect(34, 84, (220 * energy / 100.0).toInt, 13, 7, 7)
    g.setColor(Color.WHITE)
    g.setFont(new Font("SansSerif", Font.BOLD, 10))
    g.drawString("GARDEN ENERGY " + energy.toInt + "%", 264, 95)

    g.setColor(new Color(8, 15, 28, 198))
    g.fillRoundRect(18, VIEW_H - 48, VIEW_W - 36, 31, 12, 12)
    g.setFont(new Font("SansSerif", Font.PLAIN, 12))
    g.setColor(new Color(220, 245, 245))
    g.drawString("WASD / ARROWS Move   ?   SHIFT Sprint   ?   SPACE Glitch Dash   ?   E Bloom / Portal   ?   TAB Map   ?   ESC Pause", 32, VIEW_H - 28)
    if (statusTimer > 0.0) {
      val width = Math.min(VIEW_W - 100, g.getFontMetrics.stringWidth(statusMessage) + 42)
      g.setColor(new Color(11, 22, 39, 220))
      g.fillRoundRect(VIEW_W / 2 - width / 2, 142, width, 38, 18, 18)
      g.setColor(new Color(255, 255, 255, 230))
      g.setFont(new Font("SansSerif", Font.BOLD, 13))
      g.drawString(statusMessage, VIEW_W / 2 - g.getFontMetrics.stringWidth(statusMessage) / 2, 166)
    }
  }

  def drawMap(g: Graphics2D): Unit = {
    val x = VIEW_W - 182
    val y = 19
    val cell = 5
    g.setColor(new Color(5, 12, 27, 205))
    g.fillRoundRect(x - 10, y - 5, 173, 135, 14, 14)
    g.setColor(new Color(65, 245, 235, 130))
    g.drawRoundRect(x - 10, y - 5, 173, 135, 14, 14)
    g.setFont(new Font("SansSerif", Font.BOLD, 10))
    g.setColor(CYAN)
    g.drawString("GARDEN MAP", x + 3, y + 10)
    flowers.foreach { f =>
      g.setColor(if (f.bloomed) LIME else new Color(125, 165, 144))
      g.fillOval(x + f.u.toInt * cell / 1, y + f.v.toInt * cell + 13, 3, 3)
    }
    seeds.filterNot(_.collected).foreach { s =>
      g.setColor(CYAN); g.fillOval(x + s.u.toInt * cell, y + s.v.toInt * cell + 13, 3, 3)
    }
    val px = x + playerU.toInt * cell
    val py = y + playerV.toInt * cell + 13
    g.setColor(Color.WHITE); g.fillOval(px - 2, py - 2, 6, 6)
    val portalX = x + 26 * cell; val portalY = y + 19 * cell + 13
    g.setColor(PURPLE); g.fillOval(portalX - 2, portalY - 2, 6, 6)
  }

  def drawGlitch(g: Graphics2D): Unit = {
    if (glitchTimer <= 0.0) return
    g.setColor(new Color(70, 255, 235, 26))
    for (i <- 0 until 3) {
      val yy = random.nextInt(VIEW_H)
      g.fillRect(0, yy, VIEW_W, 2 + random.nextInt(5))
    }
    val yy = random.nextInt(VIEW_H)
    g.setColor(new Color(255, 55, 190, 18))
    g.fillRect(0, yy, VIEW_W, 14)
  }

  def drawGarden(g: Graphics2D): Unit = {
    drawBackground(g)
    // Draw ground back-to-front in isometric depth order.
    var depth = 0
    while (depth <= WORLD_U + WORLD_V) {
      var u = 0
      while (u <= WORLD_U) {
        val v = depth - u
        if (v >= 0 && v <= WORLD_V) drawTile(g, u, v)
        u += 1
      }
      depth += 1
    }
    val items = ArrayBuffer[RenderItem]()
    trees.foreach { t => items += RenderItem(t.u + t.v, () => drawTree(g, t)) }
    flowers.foreach { f => items += RenderItem(f.u + f.v, () => drawFlower(g, f)) }
    seeds.foreach { s => items += RenderItem(s.u + s.v, () => drawSeed(g, s)) }
    bugs.foreach { b => items += RenderItem(b.u + b.v, () => drawBug(g, b)) }
    items += RenderItem(45.0, () => drawPortal(g))
    items += RenderItem(playerU + playerV, () => drawPlayer(g))
    items.sortBy(_.depth).foreach(i => i.draw())
    drawParticles(g)
    drawHUD(g)
    if (showMap) drawMap(g)
    drawGlitch(g)
  }

  def drawButton(g: Graphics2D, rect: Rectangle, text: String, hovered: Boolean): Unit = {
    g.setColor(if (hovered) new Color(70, 255, 225, 75) else new Color(55, 150, 180, 65))
    g.fillRoundRect(rect.x, rect.y, rect.width, rect.height, 20, 20)
    g.setColor(if (hovered) Color.WHITE else CYAN)
    g.setStroke(new BasicStroke(if (hovered) 2.5f else 1.5f))
    g.drawRoundRect(rect.x, rect.y, rect.width, rect.height, 20, 20)
    g.setFont(new Font("SansSerif", Font.BOLD, 18))
    g.drawString(text, rect.x + (rect.width - g.getFontMetrics.stringWidth(text)) / 2, rect.y + 38)
  }

  def drawTitle(g: Graphics2D): Unit = {
    drawBackground(g)
    // Floating cyber petals on the title screen.
    for (i <- 0 until 22) {
      val x = (i * 59 + (animationTime * (10 + i % 5)).toInt) % (VIEW_W + 40) - 20
      val y = 260 + ((i * 73 + math.sin(animationTime + i) * 28).toInt % 390)
      val c = if (i % 2 == 0) CYAN else PINK
      g.setColor(new Color(c.getRed, c.getGreen, c.getBlue, 125))
      g.fillOval(x, y, 7 + i % 5, 12 + i % 4)
    }
    g.setColor(new Color(8, 17, 35, 150))
    g.fillRoundRect(115, 85, VIEW_W - 230, 520, 36, 36)
    g.setColor(new Color(65, 245, 235, 110))
    g.setStroke(new BasicStroke(1.6f))
    g.drawRoundRect(115, 85, VIEW_W - 230, 520, 36, 36)
    g.setFont(new Font("SansSerif", Font.BOLD, 20))
    g.setColor(LIME)
    val season = "SPRING SEASON EVENT"
    g.drawString(season, (VIEW_W - g.getFontMetrics.stringWidth(season)) / 2, 145)
    g.setFont(new Font("SansSerif", Font.BOLD, 48))
    g.setColor(Color.WHITE)
    val title = "CYBER BLOOM"
    g.drawString(title, (VIEW_W - g.getFontMetrics.stringWidth(title)) / 2, 218)
    g.setFont(new Font("SansSerif", Font.BOLD, 29))
    g.setColor(CYAN)
    val subtitle = "GLITCH IN THE GARDEN"
    g.drawString(subtitle, (VIEW_W - g.getFontMetrics.stringWidth(subtitle)) / 2, 264)
    // Decorative flower emblem.
    val cx = VIEW_W / 2
    val cy = 355
    for (i <- 0 until 8) {
      val a = i * Math.PI / 4.0 + animationTime * 0.24
      val px = cx + (Math.cos(a) * 36).toInt
      val py = cy + (Math.sin(a) * 36).toInt
      g.setColor(if (i % 2 == 0) CYAN else PINK)
      g.fillOval(px - 16, py - 16, 32, 32)
      g.setColor(new Color(255, 255, 255, 120))
      g.drawOval(px - 16, py - 16, 32, 32)
    }
    g.setColor(GOLD)
    g.fillOval(cx - 17, cy - 17, 34, 34)
    g.setFont(new Font("SansSerif", Font.PLAIN, 16))
    g.setColor(new Color(220, 242, 250))
    val line1 = "Restore the corrupted garden. Collect seeds, awaken blooms, escape through the portal."
    g.drawString(line1, (VIEW_W - g.getFontMetrics.stringWidth(line1)) / 2, 438)
    val line2 = "WASD / ARROWS Move     SHIFT Sprint     SPACE Dash     E Interact"
    g.setColor(new Color(160, 206, 221))
    g.drawString(line2, (VIEW_W - g.getFontMetrics.stringWidth(line2)) / 2, 471)
    drawButton(g, startButton, "START SPRING EVENT", startButton.contains(pointerX, pointerY))
    g.setFont(new Font("SansSerif", Font.PLAIN, 11))
    g.setColor(new Color(170, 195, 210))
    val foot = "Original procedural mini-game ? Scala + Swing ? No external assets required"
    g.drawString(foot, (VIEW_W - g.getFontMetrics.stringWidth(foot)) / 2, VIEW_H - 48)
  }

  def drawOverlay(g: Graphics2D, heading: String, sub: String, buttonText: String): Unit = {
    g.setColor(new Color(4, 8, 21, 210))
    g.fillRect(0, 0, VIEW_W, VIEW_H)
    g.setColor(CYAN)
    g.setFont(new Font("SansSerif", Font.BOLD, 42))
    g.drawString(heading, (VIEW_W - g.getFontMetrics.stringWidth(heading)) / 2, VIEW_H / 2 - 60)
    g.setColor(Color.WHITE)
    g.setFont(new Font("SansSerif", Font.PLAIN, 17))
    g.drawString(sub, (VIEW_W - g.getFontMetrics.stringWidth(sub)) / 2, VIEW_H / 2 - 18)
    val rect = new Rectangle(VIEW_W / 2 - 135, VIEW_H / 2 + 25, 270, 54)
    drawButton(g, rect, buttonText, rect.contains(pointerX, pointerY))
  }

  class GardenPanel extends JPanel {
    setPreferredSize(new Dimension(VIEW_W, VIEW_H))
    setMinimumSize(new Dimension(900, 600))
    setFocusable(true)
    setBackground(BACKGROUND)
    setDoubleBuffered(true)
    setFocusTraversalKeysEnabled(false)

    override def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val g = graphics.create().asInstanceOf[Graphics2D]
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
      g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
      // Scale the fixed scene to the actual window size.
      val sx = getWidth.toDouble / VIEW_W
      val sy = getHeight.toDouble / VIEW_H
      g.scale(sx, sy)
      if (state == "title") drawTitle(g)
      else if (state == "playing") drawGarden(g)
      else if (state == "paused") {
        drawGarden(g)
        drawOverlay(g, "PAUSED", "The garden is waiting for you.", "ESC TO RESUME  ?  R RESTART")
      }
      else if (state == "win") {
        drawGarden(g)
        drawOverlay(g, "GARDEN RESTORED!", "You collected " + collectedSeeds + " seeds and awakened " + bloomedFlowers + " flowers.", "R / ENTER PLAY AGAIN")
        g.setColor(new Color(90, 255, 220, 90))
        for (i <- 0 until 45) {
          val x = random.nextInt(VIEW_W)
          val y = random.nextInt(VIEW_H)
          g.fillOval(x, y, 3 + random.nextInt(4), 3 + random.nextInt(4))
        }
      }
      else if (state == "failed") {
        drawGarden(g)
        drawOverlay(g, "GLITCH OVERLOAD", "Energy or time ran out. The garden can still be saved.", "R / ENTER RETRY")
      }
      g.dispose()
    }
  }

  def launch(): Unit = {
    buildWorld()
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = {
        window = new JFrame("SPRING SEASON ? Cyber Bloom: Glitch in the Garden")
        panel = new GardenPanel
        panel.addKeyListener(new KeyAdapter {
          override def keyPressed(e: KeyEvent): Unit = SpringSeason_CyberBloomGlitchInTheGarden.keyPressed(e.getKeyCode)
          override def keyReleased(e: KeyEvent): Unit = keys -= e.getKeyCode
        })
        panel.addMouseListener(new MouseAdapter {
          override def mouseClicked(e: MouseEvent): Unit = {
            val sx = e.getX.toDouble * VIEW_W / math.max(1, panel.getWidth)
            val sy = e.getY.toDouble * VIEW_H / math.max(1, panel.getHeight)
            if (state == "title" && startButton.contains(sx.toInt, sy.toInt)) resetGame()
            else if (state == "paused") state = "playing"
            else if (state == "win" || state == "failed") resetGame()
            panel.requestFocusInWindow()
            panel.repaint()
          }
        })
        panel.addMouseMotionListener(new MouseAdapter {
          override def mouseMoved(e: MouseEvent): Unit = {
            pointerX = (e.getX.toDouble * VIEW_W / math.max(1, panel.getWidth)).toInt
            pointerY = (e.getY.toDouble * VIEW_H / math.max(1, panel.getHeight)).toInt
          }
          override def mouseDragged(e: MouseEvent): Unit = mouseMoved(e)
        })
        panel.addFocusListener(new FocusAdapter {
          override def focusLost(e: FocusEvent): Unit = keys.clear()
        })
        window.setContentPane(panel)
        window.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
        window.setSize(VIEW_W, VIEW_H)
        window.setMinimumSize(new Dimension(900, 600))
        window.setLocationRelativeTo(null)
        window.setVisible(true)
        panel.requestFocusInWindow()
        clock = new Timer(16, new ActionListener {
          override def actionPerformed(e: ActionEvent): Unit = tick()
        })
        clock.start()
      }
    })
  }

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


// ===== INTEGRATED GAME MODULE: BixuuDiwaliGame_MOVEMENT_CORRECTED.scala =====

import java.awt.{BasicStroke, Color, Dimension, Font, GradientPaint, Graphics, Graphics2D, RenderingHints, Polygon, RadialGradientPaint, MultipleGradientPaint}
import java.awt.event.{ActionEvent, ActionListener, KeyEvent, MouseEvent, MouseListener, WindowAdapter, WindowEvent}
import java.awt.geom.{Ellipse2D, RoundRectangle2D}
import java.awt.image.BufferedImage
import javax.swing.{AbstractAction, JComponent, JFrame, JOptionPane, JPanel, KeyStroke, SwingUtilities, Timer, WindowConstants}
import scala.util.Random

/**
 * BIXUU DIWALI TRAIN ADVENTURE
 * Standalone Scala + Java Swing/AWT game. No external libraries or image files needed.
 * Complete story: Goa letter -> ticket counter -> missed train -> TTE -> next train ->
 * three coaches and coins -> Goa station -> grandparents -> Diwali decoration and puja.
 * Controls: Swing key bindings (WHEN_IN_FOCUSED_WINDOW): WASD / Arrow Keys = move, E / ENTER / SPACE = interact,
 *           1 = Gokuldham Family (menu), 2 = Normal Family (menu), P = pause, ESC = menu.
 */
object BixuuDiwaliGame {
  def main(args: Array[String]): Unit = {
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = createWindow()
    })
  }

  private def createWindow(): Unit = {
    val frame = new JFrame("BIXUU DIWALI TRAIN ADVENTURE | Scala + Swing")
    val game = new GamePanel()
    frame.setFocusableWindowState(true)
    frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
    frame.setContentPane(game)
    frame.setSize(1100, 720)
    frame.setMinimumSize(new Dimension(900, 620))
    frame.setLocationRelativeTo(null)
    frame.setIconImage(makeAppIcon())
    // Keep keyboard control attached to the game whenever its window is activated.
    frame.addWindowListener(new WindowAdapter {
      override def windowActivated(e: WindowEvent): Unit = {
        SwingUtilities.invokeLater(new Runnable {
          override def run(): Unit = game.requestFocusInWindow()
        })
      }
      override def windowDeactivated(e: WindowEvent): Unit = game.clearHeldKeys()
    })
    frame.setVisible(true)
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = game.requestFocusInWindow()
    })
    game.startLoop()
  }

  private def makeAppIcon(): BufferedImage = {
    val img = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB)
    val g = img.createGraphics()
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
    g.setPaint(new GradientPaint(0, 0, new Color(35, 15, 65), 64, 64, new Color(230, 88, 18)))
    g.fillRoundRect(0, 0, 64, 64, 16, 16)
    g.setColor(new Color(255, 205, 77))
    g.fillOval(17, 33, 30, 13)
    g.setColor(new Color(255, 242, 170))
    g.fillOval(27, 24, 10, 15)
    g.setColor(new Color(255, 255, 225))
    g.fillOval(30, 27, 4, 10)
    g.dispose()
    img
  }

  private class DecoTarget(val x: Double, val y: Double, val label: String, var done: Boolean = false)

  private class GamePanel extends JPanel with MouseListener {
    private val rng = new Random(17)
    private var phase = "menu" // menu, playing, complete
    private var family = "Gokuldham Family"
    private var normalFamilyName = "My Family"
    private var playerName = "Bixuu"
    private var stage = 0
    private var score = 0
    private var supplies = false
    private var pujaDone = false
    private var playerX = 405.0
    private var playerY = 300.0
    private var moveUp = false
    private var moveDown = false
    private var moveLeft = false
    private var moveRight = false
    private val heldKeys = scala.collection.mutable.Set.empty[Int]
    private var walkFrame = 0
    private var tick = 0
    private var toast = "Welcome! Choose your family to begin."
    private var toastTicks = 150
    private var targets = Array.empty[DecoTarget]
    private var menuGokul = new java.awt.Rectangle(0, 0, 0, 0)
    private var menuNormal = new java.awt.Rectangle(0, 0, 0, 0)
    private var pause = false
    private var stairDescending = false
    private var stairTick = 0
    private var stairStartX = 410.0
    private var stairStartY = 305.0

    // Train-adventure story state. After Goa and the grandparents scene, the game
    // continues into the original family-specific Diwali missions below.
    private var journeyComplete = false
    private var journeyScene = "letter" // letter, ticket, missed, wait, coach, goaStation, goaHome
    private var ticketBooked = false
    private var ticketChecked = false
    private var currentCoach = 1
    private var coachCoins = Array(false, false, false)
    private var trainArrivalTicks = 0
    private var travelTicks = 0

    setFocusable(true)
    setFocusTraversalKeysEnabled(false)
    setBackground(new Color(16, 12, 35))
    addMouseListener(this)
    installKeyboardBindings()

    // Swing's WHEN_IN_FOCUSED_WINDOW bindings receive keys even when focus is on
    // another component in this game window. Each key has separate press/release
    // actions so movement continues while held and stops immediately on release.
    private def installKeyboardBindings(): Unit = {
      bindKey("W", KeyEvent.VK_W, () => moveUp = true, () => moveUp = false)
      bindKey("UP", KeyEvent.VK_UP, () => moveUp = true, () => moveUp = false)
      bindKey("S", KeyEvent.VK_S, () => moveDown = true, () => moveDown = false)
      bindKey("DOWN", KeyEvent.VK_DOWN, () => moveDown = true, () => moveDown = false)
      bindKey("A", KeyEvent.VK_A, () => moveLeft = true, () => moveLeft = false)
      bindKey("LEFT", KeyEvent.VK_LEFT, () => moveLeft = true, () => moveLeft = false)
      bindKey("D", KeyEvent.VK_D, () => moveRight = true, () => moveRight = false)
      bindKey("RIGHT", KeyEvent.VK_RIGHT, () => moveRight = true, () => moveRight = false)

      bindKey("E", KeyEvent.VK_E, () => interact(), () => ())
      bindKey("ENTER", KeyEvent.VK_ENTER, () => interact(), () => ())
      bindKey("SPACE", KeyEvent.VK_SPACE, () => interact(), () => ())
      bindKey("1", KeyEvent.VK_1, () => if (phase == "menu") startGame("Gokuldham Family"), () => ())
      bindKey("2", KeyEvent.VK_2, () => if (phase == "menu") startGame("Normal Family"), () => ())
      bindKey("P", KeyEvent.VK_P, () => {
        if (phase == "playing") {
          pause = !pause
          showToast(if (pause) "Game paused. Press P to resume." else "Game resumed.", 100)
        }
      }, () => ())
      bindKey("ESCAPE", KeyEvent.VK_ESCAPE, () => {
        phase = "menu"
        pause = false
        stairDescending = false
        clearHeldKeys()
        showToast("Choose a family to start the Goa Diwali trip.", 120)
      }, () => ())
    }

    private def bindKey(name: String, keyCode: Int, onPress: () => Unit, onRelease: () => Unit): Unit = {
      val inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
      val actionMap = getActionMap()
      val downName = "bixuu-" + name + "-down"
      val upName = "bixuu-" + name + "-up"
      inputMap.put(KeyStroke.getKeyStroke(keyCode, 0, false), downName)
      inputMap.put(KeyStroke.getKeyStroke(keyCode, 0, true), upName)
      actionMap.put(downName, new AbstractAction {
        override def actionPerformed(e: ActionEvent): Unit = {
          // Ignore operating-system key-repeat events for one-shot actions like E.
          if (!heldKeys.contains(keyCode)) {
            heldKeys += keyCode
            onPress()
          }
        }
      })
      actionMap.put(upName, new AbstractAction {
        override def actionPerformed(e: ActionEvent): Unit = {
          heldKeys -= keyCode
          onRelease()
        }
      })
    }

    def startLoop(): Unit = {
      val timer = new Timer(30, new ActionListener {
        override def actionPerformed(e: ActionEvent): Unit = {
          updateGame()
          repaint()
        }
      })
      timer.start()
    }

    private def askName(prompt: String, defaultValue: String): String = {
      val answer = JOptionPane.showInputDialog(this, prompt, defaultValue)
      if (answer == null || answer.trim.isEmpty) defaultValue else answer.trim.take(24)
    }

    private def startGame(which: String): Unit = {
      family = which
      playerName = askName("Choose your character name:", "Bixuu")
      normalFamilyName = if (family == "Normal Family") askName("Enter your family / house name:", "My Family") else "Gokuldham Society"
      phase = "playing"
      stage = 0
      score = 0
      supplies = false
      pujaDone = false
      pause = false
      stairDescending = false
      stairTick = 0
      journeyComplete = false
      journeyScene = "letter"
      ticketBooked = false
      ticketChecked = false
      currentCoach = 1
      coachCoins = Array(false, false, false)
      trainArrivalTicks = 0
      travelTicks = 0
      playerX = math.max(90.0, getWidth * 0.18)
      playerY = math.max(320.0, getHeight * 0.66)
      showToast("A letter has arrived from Goa! Find it and read your family message.", 220)
      SwingUtilities.invokeLater(new Runnable {
        override def run(): Unit = requestFocusInWindow()
      })
    }

    private def beginDiwaliMissions(): Unit = {
      journeyComplete = true
      stage = 0
      supplies = false
      pujaDone = false
      stairDescending = false
      stairTick = 0
      if (family == "Gokuldham Family") {
        playerX = 410.0
        playerY = 305.0
        targets = Array(
          new DecoTarget(505, 480, "Diyas"),
          new DecoTarget(570, 500, "Flower Toran"),
          new DecoTarget(640, 475, "Fairy Lights"),
          new DecoTarget(685, 515, "Rangoli")
        )
        showToast("You greeted your grandparents! Next: go down the stairs and start the Diwali society missions.", 230)
      } else {
        playerX = 160.0
        playerY = 480.0
        targets = Array(
          new DecoTarget(120, 475, "Diyas"),
          new DecoTarget(185, 510, "Flower Toran"),
          new DecoTarget(250, 470, "Fairy Lights"),
          new DecoTarget(310, 510, "Rangoli")
        )
        showToast("You reached your grandparents in Goa! Buy supplies and decorate your family home for Diwali.", 230)
      }
      requestFocusInWindow()
    }

    private def showToast(message: String, ticks: Int): Unit = {
      toast = message
      toastTicks = ticks
    }

    private def distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = {
      val dx = x1 - x2
      val dy = y1 - y2
      math.sqrt(dx * dx + dy * dy)
    }

    private def updateGame(): Unit = {
      tick += 1
      if (toastTicks > 0) toastTicks -= 1
      if (phase != "playing" || pause) return

      if (!journeyComplete) {
        if (journeyScene == "wait" && trainArrivalTicks < 100) trainArrivalTicks += 1
        if (journeyScene == "coach" && travelTicks < 360) travelTicks += 1
        var jdx = 0.0
        var jdy = 0.0
        if (moveLeft) jdx -= 1.0
        if (moveRight) jdx += 1.0
        if (moveUp) jdy -= 1.0
        if (moveDown) jdy += 1.0
        if (jdx != 0.0 || jdy != 0.0) {
          val jlen = math.sqrt(jdx * jdx + jdy * jdy)
          playerX += jdx / jlen * 3.3
          playerY += jdy / jlen * 3.3
          playerX = math.max(30.0, math.min(getWidth - 30.0, playerX))
          playerY = math.max(270.0, math.min(getHeight - 93.0, playerY))
          walkFrame += 1
        }
        return
      }

      if (stairDescending) {
        stairTick += 1
        val progress = math.min(1.0, stairTick / 34.0)
        playerX = stairStartX + (420.0 - stairStartX) * progress
        playerY = stairStartY + (400.0 - stairStartY) * progress
        walkFrame += 1
        if (progress >= 1.0) {
          stairDescending = false
          stage = 1
          score += 20
          showToast("You came down the stairs! Now ask the society secretary for permission.", 190)
        }
        return
      }

      var dx = 0.0
      var dy = 0.0
      if (moveLeft) dx -= 1.0
      if (moveRight) dx += 1.0
      if (moveUp) dy -= 1.0
      if (moveDown) dy += 1.0
      if (dx != 0.0 || dy != 0.0) {
        val len = math.sqrt(dx * dx + dy * dy)
        playerX += dx / len * 3.2
        playerY += dy / len * 3.2
        playerX = math.max(34.0, math.min(getWidth - 34.0, playerX))
        playerY = math.max(270.0, math.min(getHeight - 98.0, playerY))
        walkFrame += 1
      }
    }

    private def interact(): Unit = {
      if (stairDescending) return
      if (phase == "menu") {
        startGame(family)
        return
      }
      if (phase == "complete") {
        phase = "menu"
        showToast("Choose a family and start a new Diwali adventure!", 150)
        return
      }
      if (pause) {
        pause = false
        showToast("Game resumed.", 80)
        return
      }

      if (!journeyComplete) {
        interactJourney()
        return
      }

      if (family == "Gokuldham Family") {
        stage match {
          case 0 =>
            if (distance(playerX, playerY, 420, 355) < 100) {
              stairDescending = true
              stairTick = 0
              stairStartX = playerX
              stairStartY = playerY
              showToast("Walking down the staircase...", 75)
            } else showToast("Walk near the staircase (middle building) and press E.", 100)
          case 1 =>
            if (distance(playerX, playerY, 535, 430) < 90) {
              stage = 2
              score += 30
              showToast("Secretary: Permission granted! Visit Abdul's shop for supplies.", 190)
            } else showToast("Go near the society secretary beside the apartment and press E.", 110)
          case 2 =>
            if (distance(playerX, playerY, getWidth - 175, 420) < 105) buySupplies()
            else showToast("Walk to Abdul's shop on the right and press E.", 110)
          case 3 => decorateAtNearestTarget()
          case 4 =>
            if (distance(playerX, playerY, 535, 535) < 95) finishPuja()
            else showToast("Go to the community puja area and press E.", 110)
          case _ =>
            phase = "complete"
        }
      } else {
        stage match {
          case 0 =>
            if (distance(playerX, playerY, getWidth - 175, 420) < 105) buySupplies()
            else showToast("Go to Abdul's shop on the right to buy your supplies.", 110)
          case 1 => decorateAtNearestTarget()
          case 2 =>
            if (distance(playerX, playerY, 225, 540) < 100) finishPuja()
            else showToast("Return to your home puja area and press E.", 110)
          case _ => phase = "complete"
        }
      }
    }

    private def interactJourney(): Unit = {
      val w = getWidth.toDouble
      val h = getHeight.toDouble
      journeyScene match {
        case "letter" =>
          if (distance(playerX, playerY, w * 0.70, h * 0.63) < 112.0) {
            journeyScene = "ticket"
            playerX = w * 0.12
            playerY = h * 0.66
            score += 10
            showToast("Letter: Come to Goa for Diwali! Your grandparents are waiting. Go to the station ticket counter.", 240)
          } else showToast("Find the glowing letter on the table and press E to read it.", 120)

        case "ticket" =>
          if (distance(playerX, playerY, w * 0.78, h * 0.60) < 115.0) {
            ticketBooked = true
            journeyScene = "missed"
            playerX = w * 0.12
            playerY = h * 0.66
            score += 20
            showToast("Ticket booked! Oh no ? your train has already left. Find the TTE on the platform.", 220)
          } else showToast("Go to the ticket counter, then press E to book your Goa ticket.", 120)

        case "missed" =>
          if (distance(playerX, playerY, w * 0.70, h * 0.62) < 112.0) {
            journeyScene = "wait"
            trainArrivalTicks = 0
            playerX = w * 0.14
            playerY = h * 0.66
            score += 15
            showToast("TTE: This train has gone. Another train is coming soon ? wait near the marked platform door.", 220)
          } else showToast("Talk to the TTE (Train Ticket Examiner) to ask about the next train.", 125)

        case "wait" =>
          if (trainArrivalTicks < 100) {
            showToast("The next train is on its way. Please wait a little while on the platform.", 100)
          } else if (distance(playerX, playerY, w * 0.53, h * 0.62) < 145.0) {
            journeyScene = "coach"
            currentCoach = 1
            ticketChecked = false
            travelTicks = 0
            playerX = w * 0.12
            playerY = h * 0.64
            showToast("Boarded the next train! You are in Coach 1. Keep your luggage close and show your ticket to the TTE.", 220)
          } else showToast("The train has arrived! Walk to its open door near the middle of the platform and press E.", 130)

        case "coach" =>
          val coinX = w * 0.44
          val coinY = h * 0.57
          if (!coachCoins(currentCoach - 1) && distance(playerX, playerY, coinX, coinY) < 72.0) {
            coachCoins(currentCoach - 1) = true
            score += 25
            showToast("Coin collected! " + coachCoins.count(identity) + "/3 train coins collected.", 130)
          } else if (currentCoach == 1 && !ticketChecked && distance(playerX, playerY, w * 0.70, h * 0.59) < 105.0) {
            ticketChecked = true
            score += 35
            showToast("TTE: Ticket verified. You are in Coach 1 ? now cross the coaches and collect the bonus coins.", 220)
          } else if (distance(playerX, playerY, w * 0.88, h * 0.64) < 100.0) {
            if (!ticketChecked) showToast("Show your ticket to the TTE in Coach 1 first.", 130)
            else if (currentCoach < 3) {
              currentCoach += 1
              playerX = w * 0.12
              playerY = h * 0.64
              showToast("You entered Coach " + currentCoach + ". Look around for the glowing coin, then move to the next coach.", 180)
            } else if (travelTicks < 220) {
              showToast("The train is delayed at a signal. Stay aboard ? journey progress: " + math.min(100, travelTicks * 100 / 220) + "%.", 130)
            } else {
              journeyScene = "goaStation"
              playerX = w * 0.12
              playerY = h * 0.66
              score += 50
              showToast("The train has arrived in Goa! Leave the train and find the station exit.", 210)
            }
          } else {
            val hint = if (currentCoach == 1 && !ticketChecked) "Find the TTE in Coach 1 and show your ticket." else "Walk through the coach to collect the coin or reach the door at the right."
            showToast(hint, 120)
          }

        case "goaStation" =>
          if (distance(playerX, playerY, w * 0.84, h * 0.64) < 125.0) {
            journeyScene = "goaHome"
            playerX = w * 0.13
            playerY = h * 0.66
            score += 20
            showToast("You are outside Goa station. Walk to your grandparents' house and greet Aaji-Ajoba.", 220)
          } else showToast("Walk to the glowing station exit on the right and press E.", 120)

        case "goaHome" =>
          if (distance(playerX, playerY, w * 0.70, h * 0.60) < 120.0) {
            score += 50
            showToast("Aaji-Ajoba: Welcome home! Happy Diwali! Let us prepare the house and celebrate together.", 180)
            beginDiwaliMissions()
          } else showToast("Walk up to your grandparents near the front door and press E to greet them.", 120)

        case _ => showToast("Follow the glowing marker to continue your adventure.", 100)
      }
    }

    private def buySupplies(): Unit = {
      if (!supplies) {
        supplies = true
        score += 50
        if (family == "Gokuldham Family") stage = 3 else stage = 1
        showToast("Abdul: Supplies collected! Demo contact only: 000-000-0000 (not real).", 190)
      } else {
        showToast("Abdul: You already have all the decoration supplies. Happy Diwali!", 120)
      }
    }

    private def decorateAtNearestTarget(): Unit = {
      if (!supplies) {
        showToast("First collect decoration supplies from Abdul's shop!", 120)
        return
      }
      var nearest: DecoTarget = null
      var nearestD = 99999.0
      targets.foreach { t =>
        if (!t.done) {
          val d = distance(playerX, playerY, t.x, t.y)
          if (d < nearestD) { nearestD = d; nearest = t }
        }
      }
      if (nearest != null && nearestD < 82.0) {
        nearest.done = true
        score += 25
        val remaining = targets.count(t => !t.done)
        if (remaining == 0) {
          if (family == "Gokuldham Family") stage = 4 else stage = 2
          showToast("Amazing! All 4 decorations are complete. Go to the puja area.", 190)
        } else {
          showToast(nearest.label + " decorated! " + (4 - remaining) + "/4 complete. Find the next marker.", 135)
        }
      } else {
        val next = targets.find(t => !t.done)
        if (next.isDefined) showToast("Go near the glowing " + next.get.label + " marker and press E.", 110)
        else showToast("All decorations complete. Go to the puja area.", 100)
      }
    }

    private def finishPuja(): Unit = {
      if (!supplies) {
        showToast("Collect the supplies before starting the puja.", 110)
      } else if (targets.exists(t => !t.done)) {
        showToast("Finish all 4 decoration tasks before the puja.", 110)
      } else {
        pujaDone = true
        score += 100
        phase = "complete"
        showToast("Shubh Deepawali! Mission completed!", 250)
      }
    }

    private def currentTask: String = {
      if (family == "Gokuldham Family") {
        stage match {
          case 0 => "MISSION 1 / 5  ?  ??????? ??????? ?? ??? E ????"
          case 1 => "MISSION 2 / 5  ?  Society secretary ???? permission ????"
          case 2 => "MISSION 3 / 5  ?  Abdul ???? ????????? ????? ????"
          case 3 => "MISSION 4 / 5  ?  4 decoration markers ????? ??? (" + targets.count(_.done) + "/4)"
          case 4 => "MISSION 5 / 5  ?  Community puja ????? ???"
          case _ => "All missions complete!"
        }
      } else {
        stage match {
          case 0 => "MISSION 1 / 3  ?  Abdul ???? ????????? ????? ????? ???"
          case 1 => "MISSION 2 / 3  ?  ????? ?? ???? (" + targets.count(_.done) + "/4)"
          case 2 => "MISSION 3 / 3  ?  ??? ???? ????? ???"
          case _ => "All missions complete!"
        }
      }
    }

    override def paintComponent(raw: Graphics): Unit = {
      super.paintComponent(raw)
      val g = raw.asInstanceOf[Graphics2D]
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
      g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
      if (phase == "menu") drawMenu(g)
      else {
        if (!journeyComplete) {
          drawJourneyScene(g)
          drawJourneyHUD(g)
        } else {
          drawWorld(g)
          drawHUD(g)
        }
        if (phase == "complete") drawComplete(g)
        if (pause) drawPause(g)
      }
    }

    private def font(size: Int, style: Int = Font.PLAIN): Font = new Font("SansSerif", style, size)

    private def drawMenu(g: Graphics2D): Unit = {
      val w = getWidth
      val h = getHeight
      g.setPaint(new GradientPaint(0, 0, new Color(17, 10, 45), 0, h, new Color(84, 27, 62)))
      g.fillRect(0, 0, w, h)
      drawStars(g, 0, 0, w, h, 78)
      // Soft festive moon
      g.setColor(new Color(255, 224, 146, 35))
      g.fillOval(w - 180, 35, 120, 120)
      g.setColor(new Color(255, 235, 176))
      g.fillOval(w - 153, 62, 66, 66)
      drawDiyas(g, 52, h - 102, 1.0)
      drawDiyas(g, w - 110, h - 102, 1.0)

      g.setFont(font(42, Font.BOLD))
      g.setColor(new Color(255, 200, 67))
      centered(g, "BIXUU DIWALI TRAIN ADVENTURE", w / 2, 105)
      g.setFont(font(18, Font.BOLD))
      g.setColor(new Color(255, 239, 206))
      centered(g, "LETTER ? TICKET ? TRAIN ? GOA ? GRANDPARENTS ? DIWALI", w / 2, 143)
      g.setFont(font(15, Font.PLAIN))
      g.setColor(new Color(227, 218, 246))
      centered(g, "Travel to Goa, explore three train coaches, then celebrate Diwali", w / 2, 180)

      val cardW = math.min(360, (w - 90) / 2)
      val cardH = 235
      val gap = 28
      val left = (w - (cardW * 2 + gap)) / 2
      val top = 220
      menuGokul = new java.awt.Rectangle(left, top, cardW, cardH)
      menuNormal = new java.awt.Rectangle(left + cardW + gap, top, cardW, cardH)
      drawFamilyCard(g, menuGokul, "GOKULDHAM FAMILY", "Train + Society missions", "Goa trip ? Stairs ? Permission", "Abdul's shop ? Decorate ? Puja", true)
      drawFamilyCard(g, menuNormal, "NORMAL FAMILY", "Train + Home missions", "Goa trip ? Grandparents' home", "Buy supplies ? Decorate ? Puja", false)

      g.setFont(font(15, Font.BOLD))
      g.setColor(new Color(255, 220, 124))
      centered(g, "SELECT A FAMILY TO START THE GOA TRIP  ?  CLICK A CARD OR PRESS 1 / 2", w / 2, top + cardH + 38)
      g.setFont(font(13, Font.PLAIN))
      g.setColor(new Color(221, 211, 238))
      centered(g, "Click game window first ? HOLD WASD / ARROWS = Move ? E / ENTER / SPACE = Interact", w / 2, h - 27)
    }

    private def drawFamilyCard(g: Graphics2D, r: java.awt.Rectangle, heading: String, sub: String, line1: String, line2: String, society: Boolean): Unit = {
      g.setPaint(new GradientPaint(r.x, r.y, new Color(57, 39, 84), r.x + r.width, r.y + r.height, new Color(29, 23, 59)))
      g.fill(new RoundRectangle2D.Double(r.x, r.y, r.width, r.height, 28, 28))
      g.setStroke(new BasicStroke(2.0f))
      g.setColor(if (society) new Color(255, 185, 64) else new Color(71, 219, 193))
      g.draw(new RoundRectangle2D.Double(r.x, r.y, r.width, r.height, 28, 28))
      if (society) drawMiniBuilding(g, r.x + r.width / 2 - 48, r.y + 20, 96, 72)
      else drawMiniHouse(g, r.x + r.width / 2 - 46, r.y + 26, 92, 66)
      g.setFont(font(21, Font.BOLD))
      g.setColor(new Color(255, 219, 120))
      centered(g, heading, r.x + r.width / 2, r.y + 121)
      g.setFont(font(16, Font.BOLD))
      g.setColor(Color.WHITE)
      centered(g, sub, r.x + r.width / 2, r.y + 150)
      g.setFont(font(12, Font.PLAIN))
      g.setColor(new Color(226, 219, 242))
      centered(g, line1, r.x + r.width / 2, r.y + 176)
      centered(g, line2, r.x + r.width / 2, r.y + 195)
      g.setFont(font(12, Font.BOLD))
      g.setColor(if (society) new Color(255, 198, 86) else new Color(106, 237, 210))
      centered(g, if (society) "CLICK TO PLAY  (1)" else "CLICK TO PLAY  (2)", r.x + r.width / 2, r.y + 219)
    }

    private def drawJourneyScene(g: Graphics2D): Unit = {
      val w = getWidth
      val h = getHeight
      g.setPaint(new GradientPaint(0, 0, new Color(16, 20, 59), 0, h, new Color(82, 37, 74)))
      g.fillRect(0, 0, w, h)
      drawStars(g, 0, 0, w, 255, 48)
      g.setColor(new Color(255, 221, 155, 35)); g.fillOval(w - 175, 28, 105, 105)
      g.setColor(new Color(255, 231, 171)); g.fillOval(w - 149, 54, 55, 55)
      g.setPaint(new GradientPaint(0, 260, new Color(100, 92, 125), 0, h, new Color(35, 38, 61)))
      g.fillRect(0, 260, w, h - 260)
      // A subtle perspective grid gives the hand-drawn world a dimensional feel.
      g.setColor(new Color(255, 255, 255, 16))
      var gx = -40
      while (gx < w + 100) { g.drawLine(gx, 330, gx + 125, h); gx += 100 }
      var gy = 350
      while (gy < h) { g.drawLine(0, gy, w, gy); gy += 46 }

      journeyScene match {
        case "letter" =>
          drawMiniHouse(g, 70, 300, 280, 190)
          g.setColor(new Color(86, 48, 52)); g.fillRoundRect((w * 0.58).toInt, 400, 180, 95, 12, 12)
          g.setColor(new Color(188, 111, 72)); g.fillRect((w * 0.57).toInt, 390, 190, 16)
          g.setColor(new Color(255, 232, 185)); g.fillRoundRect((w * 0.66).toInt, 370, 66, 52, 6, 6)
          g.setColor(new Color(174, 48, 52)); g.fillRoundRect((w * 0.67).toInt, 379, 48, 32, 4, 4)
          g.setColor(new Color(255, 224, 137)); g.drawLine((w * 0.69).toInt, 386, (w * 0.72).toInt, 386)
          g.drawLine((w * 0.69).toInt, 393, (w * 0.72).toInt, 393)
          drawMarker(g, (w * 0.70).toInt, (h * 0.63).toInt - 28, new Color(255, 214, 86))
          drawPerson(g, (w * 0.43).toDouble, (h * 0.62).toDouble, new Color(77, 159, 184), new Color(242, 190, 143), false, tick)
          g.setFont(font(16, Font.BOLD)); g.setColor(new Color(255, 231, 166)); centered(g, "LETTER FROM GOA", (w * 0.70).toInt, 355)
          g.setFont(font(13, Font.PLAIN)); g.setColor(Color.WHITE); centered(g, "Your grandparents invited you for Diwali", (w * 0.52).toInt, 545)

        case "ticket" =>
          drawStationFacade(g, w, h, "CITY RAILWAY STATION")
          drawRailTracks(g, w, h)
          val cx = (w * 0.68).toInt; val cy = 345
          g.setColor(new Color(47, 42, 72)); g.fillRoundRect(cx - 15, cy, 265, 126, 14, 14)
          g.setColor(new Color(243, 167, 77)); g.fillRoundRect(cx - 15, cy - 18, 265, 35, 10, 10)
          g.setFont(font(18, Font.BOLD)); g.setColor(new Color(62, 36, 55)); centered(g, "TICKET COUNTER", cx + 117, cy + 5)
          g.setColor(new Color(255, 236, 196)); g.fillRoundRect(cx + 70, cy + 35, 90, 40, 7, 7)
          g.setColor(new Color(91, 64, 76)); g.drawRect(cx + 80, cy + 43, 70, 21)
          drawPerson(g, (w * 0.78).toDouble, (h * 0.60).toDouble, new Color(64, 137, 190), new Color(245, 190, 140), false, tick)
          drawPlayer(g)

        case "missed" =>
          drawStationFacade(g, w, h, "PLATFORM 1 ? DEPARTED")
          drawRailTracks(g, w, h)
          val trainX = w - ((tick * 7) % (w + 600)) - 480
          drawPassengerTrain(g, trainX, 365, 146, 88)
          drawPerson(g, (w * 0.70).toDouble, (h * 0.62).toDouble, new Color(43, 111, 170), new Color(244, 194, 145), false, tick)
          g.setFont(font(12, Font.BOLD)); g.setColor(new Color(255, 229, 165)); centered(g, "TTE", (w * 0.70).toInt, (h * 0.62).toInt + 38)
          g.setFont(font(16, Font.BOLD)); g.setColor(new Color(255, 206, 108)); centered(g, "OH NO! THE TRAIN HAS LEFT", w / 2, 305)
          drawPlayer(g)

        case "wait" =>
          drawStationFacade(g, w, h, "PLATFORM 2 ? NEXT TRAIN")
          drawRailTracks(g, w, h)
          val prog = math.min(1.0, trainArrivalTicks / 100.0)
          val tx = ((w + 80) + (w * 0.30 - (w + 80)) * prog).toInt
          drawPassengerTrain(g, tx, 365, 146, 88)
          drawPerson(g, (w * 0.26).toDouble, (h * 0.62).toDouble, new Color(43, 111, 170), new Color(244, 194, 145), false, tick)
          g.setFont(font(12, Font.BOLD)); g.setColor(new Color(255, 229, 165)); centered(g, "TTE: NEXT TRAIN TO GOA", (w * 0.26).toInt, (h * 0.62).toInt + 38)
          if (trainArrivalTicks >= 100) drawMarker(g, (w * 0.53).toInt, (h * 0.62).toInt - 35, new Color(100, 248, 193))
          g.setFont(font(14, Font.BOLD)); g.setColor(new Color(255, 220, 130))
          centered(g, if (trainArrivalTicks < 100) "NEXT TRAIN ARRIVING... " + trainArrivalTicks + "%" else "TRAIN ARRIVED ? BOARD THE OPEN DOOR", w / 2, 300)
          drawPlayer(g)

        case "coach" =>
          g.setPaint(new GradientPaint(0, 260, new Color(36, 47, 82), 0, h, new Color(23, 29, 50)))
          g.fillRect(0, 260, w, h - 260)
          // Windows with a moving Goa countryside view.
          var i = 0
          while (i < 5) {
            val wx = 28 + i * ((w - 55) / 5)
            g.setColor(new Color(15, 30, 58)); g.fillRoundRect(wx, 300, 138, 82, 10, 10)
            g.setPaint(new GradientPaint(wx, 302, new Color(113, 197, 212), wx, 380, new Color(77, 145, 109)))
            g.fillRoundRect(wx + 5, 305, 128, 72, 8, 8)
            val slide = (tick * 2 + i * 41) % 150
            g.setColor(new Color(41, 119, 75)); g.fillOval(wx + slide - 15, 335, 35, 45)
            g.setColor(new Color(37, 105, 67)); g.fillRect(wx + slide, 325, 7, 54)
            g.setColor(new Color(22, 32, 58)); g.drawRoundRect(wx, 300, 138, 82, 10, 10)
            i += 1
          }
          g.setColor(new Color(255, 212, 124)); g.fillRect(0, 393, w, 7)
          g.setPaint(new GradientPaint(0, 400, new Color(67, 77, 104), 0, h, new Color(36, 40, 62)))
          g.fillRect(0, 400, w, h - 400)
          // seats on both sides and the coach passage.
          var sx = 25
          while (sx < w - 80) {
            g.setColor(new Color(21, 29, 57)); g.fillRoundRect(sx, 420, 75, 92, 12, 12)
            g.setColor(new Color(178, 63, 79)); g.fillRoundRect(sx + 5, 425, 65, 42, 9, 9)
            g.setColor(new Color(107, 42, 63)); g.fillRect(sx + 5, 468, 65, 28)
            g.setColor(new Color(205, 147, 91)); g.fillRoundRect(sx + 10, 492, 55, 8, 4, 4)
            sx += 145
          }
          g.setColor(new Color(18, 24, 42)); g.fillRoundRect(w - 105, 390, 72, 180, 9, 9)
          g.setColor(new Color(255, 194, 87)); g.drawRoundRect(w - 105, 390, 72, 180, 9, 9)
          g.setFont(font(15, Font.BOLD)); g.setColor(new Color(255, 223, 143)); centered(g, "COACH " + currentCoach, w / 2, 285)
          g.setFont(font(11, Font.BOLD)); g.setColor(new Color(235, 232, 255)); centered(g, "KEEP YOUR LUGGAGE SAFE", w / 2, 530)
          if (currentCoach == 1 && !ticketChecked) {
            drawPerson(g, (w * 0.70).toDouble, (h * 0.59).toDouble, new Color(43, 111, 170), new Color(244, 194, 145), false, tick)
            g.setFont(font(11, Font.BOLD)); g.setColor(new Color(255, 229, 165)); centered(g, "TTE", (w * 0.70).toInt, (h * 0.59).toInt + 34)
            drawMarker(g, (w * 0.70).toInt, (h * 0.59).toInt - 35, new Color(98, 242, 192))
          }
          if (!coachCoins(currentCoach - 1)) {
            val cx = (w * 0.44).toInt; val cy = (h * 0.57).toInt
            g.setColor(new Color(255, 205, 55, 60)); g.fillOval(cx - 20, cy - 20, 40, 40)
            g.setColor(new Color(255, 198, 42)); g.fillOval(cx - 10, cy - 10, 20, 20)
            g.setColor(new Color(255, 244, 163)); g.drawOval(cx - 6, cy - 6, 12, 12)
            g.setFont(font(10, Font.BOLD)); g.setColor(new Color(255, 236, 163)); centered(g, "COIN", cx, cy + 28)
          }
          g.setColor(new Color(255, 218, 115)); g.setFont(font(12, Font.BOLD)); g.drawString("COACH " + currentCoach + " / 3", 25, h - 98)
          g.setColor(new Color(131, 245, 205)); g.drawString("Journey: " + math.min(100, travelTicks * 100 / 220) + "%", 25, h - 78)
          drawPlayer(g)

        case "goaStation" =>
          drawStationFacade(g, w, h, "GOA ? MADGAON STATION")
          drawRailTracks(g, w, h)
          drawPassengerTrain(g, 40, 365, 146, 88)
          // Palm silhouettes make the destination recognizable.
          drawPalm(g, w - 210, 285); drawPalm(g, w - 105, 305)
          drawMarker(g, (w * 0.84).toInt, (h * 0.64).toInt - 30, new Color(100, 248, 193))
          g.setFont(font(14, Font.BOLD)); g.setColor(new Color(255, 225, 144)); centered(g, "EXIT TO GOA", (w * 0.84).toInt, 340)
          drawPlayer(g)

        case "goaHome" =>
          // Tropical lane, a Goan family home, and the waiting grandparents.
          g.setColor(new Color(30, 80, 71)); g.fillRect(0, 260, w, h - 260)
          g.setColor(new Color(63, 117, 82)); g.fillOval(35, 280, 200, 125); g.fillOval(w - 260, 275, 225, 140)
          drawPalm(g, 70, 310); drawPalm(g, w - 95, 315)
          drawMiniHouse(g, (w * 0.32).toInt, 318, 300, 210)
          drawToran(g, (w * 0.32).toInt + 100, 385, 100, 19, true)
          drawDiyas(g, (w * 0.32).toInt + 60, 505, 0.65)
          drawRangoli(g, (w * 0.32).toInt + 250, 545)
          drawPerson(g, (w * 0.70).toDouble, (h * 0.60).toDouble, new Color(155, 88, 160), new Color(232, 179, 133), false, tick)
          drawPerson(g, (w * 0.78).toDouble, (h * 0.60).toDouble, new Color(65, 143, 104), new Color(236, 188, 142), false, tick + 4)
          g.setFont(font(12, Font.BOLD)); g.setColor(new Color(255, 241, 199)); centered(g, "AAJI", (w * 0.70).toInt, (h * 0.60).toInt + 35)
          centered(g, "AJOBA", (w * 0.78).toInt, (h * 0.60).toInt + 35)
          drawMarker(g, (w * 0.72).toInt, (h * 0.60).toInt - 38, new Color(255, 215, 84))
          g.setFont(font(15, Font.BOLD)); g.setColor(new Color(255, 230, 165)); centered(g, "AAJI-AJOBA'S HOUSE ? GOA", w / 2, 285)
          drawPlayer(g)

        case _ => drawWorld(g)
      }
      drawAmbientEffects(g)
    }

    private def drawJourneyHUD(g: Graphics2D): Unit = {
      val w = getWidth
      g.setColor(new Color(12, 12, 31, 225)); g.fillRoundRect(14, 12, w - 28, 88, 18, 18)
      g.setColor(new Color(255, 199, 79)); g.setStroke(new BasicStroke(1.5f)); g.drawRoundRect(14, 12, w - 28, 88, 18, 18)
      g.setFont(font(18, Font.BOLD)); g.setColor(new Color(255, 212, 108)); g.drawString("BIXUU TRAIN ADVENTURE", 30, 38)
      g.setFont(font(11, Font.BOLD)); g.setColor(Color.WHITE); g.drawString(playerName + " ? " + family, 31, 57)
      g.setFont(font(13, Font.BOLD)); g.setColor(new Color(131, 245, 205)); g.drawString("SCORE " + score + "    COINS " + coachCoins.count(identity) + "/3", 31, 79)
      val task = journeyScene match {
        case "letter" => "MISSION 1/7: Find the letter from Goa and read it."
        case "ticket" => "MISSION 2/7: Go to the railway ticket counter."
        case "missed" => "MISSION 3/7: The train left ? ask the TTE about another train."
        case "wait" => "MISSION 4/7: Wait for the next train, then board it."
        case "coach" => "MISSION 5/7: Show your ticket, cross 3 coaches and collect coins."
        case "goaStation" => "MISSION 6/7: Exit Goa station and find the way home."
        case "goaHome" => "MISSION 7/7: Walk to Aaji-Ajoba and greet them."
        case _ => "Continue your adventure."
      }
      g.setFont(font(13, Font.BOLD)); g.setColor(new Color(255, 243, 219)); g.drawString(task, 285, 39)
      g.setFont(font(11, Font.PLAIN)); g.setColor(new Color(222, 218, 240)); g.drawString("Click game first ? HOLD WASD/Arrows: Move ? E/Enter: Interact ? P: Pause ? ESC: Menu", 285, 61)
      if (journeyScene == "coach") {
        g.setColor(new Color(255, 211, 107)); g.drawString("Coach " + currentCoach + "/3  ?  Ticket " + (if (ticketChecked) "verified" else "not checked") + "  ?  Journey " + math.min(100, travelTicks * 100 / 220) + "%", 285, 81)
      } else if (journeyScene == "wait") {
        g.setColor(new Color(255, 211, 107)); g.drawString("Train arrival: " + trainArrivalTicks + "%", 285, 81)
      } else {
        g.setColor(new Color(255, 211, 107)); g.drawString("Goa family trip ? Diwali celebration", 285, 81)
      }
      if (toastTicks > 0 && toast != null && toast.nonEmpty) {
        g.setFont(font(13, Font.BOLD))
        val tw = math.min(w - 38, g.getFontMetrics.stringWidth(toast) + 34)
        g.setColor(new Color(15, 12, 34, 232)); g.fillRoundRect((w - tw) / 2, getHeight - 67, tw, 32, 12, 12)
        g.setColor(new Color(255, 207, 93)); g.drawRoundRect((w - tw) / 2, getHeight - 67, tw, 32, 12, 12)
        g.setColor(new Color(255, 245, 222)); centered(g, toast, w / 2, getHeight - 46)
      }
      g.setFont(font(11, Font.BOLD)); g.setColor(new Color(255, 230, 156))
      centered(g, "CLICK GAME WINDOW FIRST ? HOLD WASD / ARROWS TO MOVE ? WALK CLOSE ? PRESS E / ENTER TO USE OR PICK UP", w / 2, getHeight - 25)
    }

    private def drawStationFacade(g: Graphics2D, w: Int, h: Int, title: String): Unit = {
      g.setColor(new Color(38, 43, 76)); g.fillRect(0, 250, w, 130)
      g.setColor(new Color(255, 192, 91)); g.fillRect(0, 250, w, 10)
      var bx = 18
      while (bx < w) {
        g.setColor(new Color(62, 66, 98)); g.fillRoundRect(bx, 272, 125, 79, 12, 12)
        g.setColor(new Color(255, 220, 143, 160)); g.fillRect(bx + 11, 285, 103, 8)
        g.setColor(new Color(28, 39, 66)); g.fillRect(bx + 20, 307, 84, 29)
        bx += 145
      }
      g.setColor(new Color(24, 24, 46)); g.fillRoundRect(w / 2 - 230, 205, 460, 56, 15, 15)
      g.setColor(new Color(255, 206, 100)); g.setStroke(new BasicStroke(2f)); g.drawRoundRect(w / 2 - 230, 205, 460, 56, 15, 15)
      g.setFont(font(21, Font.BOLD)); g.setColor(new Color(255, 228, 158)); centered(g, title, w / 2, 240)
    }

    private def drawRailTracks(g: Graphics2D, w: Int, h: Int): Unit = {
      val y = h - 122
      g.setColor(new Color(43, 45, 58)); g.fillRect(0, y - 7, w, 42)
      g.setColor(new Color(199, 202, 208)); g.setStroke(new BasicStroke(4f)); g.drawLine(0, y, w, y); g.drawLine(0, y + 24, w, y + 24)
      g.setColor(new Color(113, 94, 91))
      var x = 0
      while (x < w) { g.fillRect(x, y - 8, 11, 40); x += 27 }
      g.setColor(new Color(221, 198, 160, 130)); g.fillRect(0, y - 22, w, 5)
    }

    private def drawPassengerTrain(g: Graphics2D, x: Int, y: Int, coachW: Int, coachH: Int): Unit = {
      // Animated stylized passenger train, built completely from AWT shapes.
      val totalW = coachW * 3 + 84
      g.setColor(new Color(7, 9, 20, 95)); g.fillOval(x - 8, y + coachH + 28, totalW + 25, 23)
      var i = 0
      while (i < 3) {
        val cx = x + i * coachW
        g.setPaint(new GradientPaint(cx, y, new Color(211, 225, 230), cx, y + coachH, new Color(70, 119, 164)))
        g.fillRoundRect(cx, y, coachW - 8, coachH, 13, 13)
        g.setColor(new Color(207, 56, 67)); g.fillRect(cx + 3, y + coachH - 18, coachW - 14, 8)
        var wx = cx + 12
        while (wx < cx + coachW - 25) {
          g.setColor(new Color(27, 49, 76)); g.fillRoundRect(wx, y + 13, 22, 27, 4, 4)
          g.setColor(new Color(154, 217, 233)); g.fillRect(wx + 3, y + 16, 16, 5)
          wx += 31
        }
        g.setColor(new Color(36, 46, 66)); g.fillOval(cx + 20, y + coachH - 4, 25, 25); g.fillOval(cx + coachW - 45, y + coachH - 4, 25, 25)
        g.setColor(new Color(176, 183, 196)); g.fillOval(cx + 26, y + coachH + 2, 13, 13); g.fillOval(cx + coachW - 39, y + coachH + 2, 13, 13)
        i += 1
      }
      val ex = x + coachW * 3
      g.setPaint(new GradientPaint(ex, y + 10, new Color(225, 222, 209), ex + 84, y + coachH, new Color(54, 101, 149)))
      g.fillRoundRect(ex, y + 9, 84, coachH - 1, 18, 18)
      g.setColor(new Color(255, 201, 74)); g.fillRoundRect(ex + 8, y + coachH - 30, 67, 12, 5, 5)
      g.setColor(new Color(28, 45, 70)); g.fillRoundRect(ex + 15, y + 18, 47, 25, 5, 5)
      g.setColor(new Color(255, 244, 190)); g.fillOval(ex + 10, y + 44, 10, 10); g.fillOval(ex + 66, y + 44, 10, 10)
    }

    private def drawPalm(g: Graphics2D, x: Int, y: Int): Unit = {
      g.setColor(new Color(103, 71, 48)); g.fillPolygon(new Polygon(Array(x - 4, x + 5, x + 10, x + 3), Array(y + 72, y + 72, y, y), 4))
      g.setColor(new Color(37, 110, 69))
      g.fillArc(x - 47, y - 20, 60, 38, 25, 135); g.fillArc(x - 20, y - 32, 62, 40, 15, 140)
      g.fillArc(x - 49, y - 4, 64, 37, 190, 130); g.fillArc(x - 22, y - 8, 62, 37, 210, 120)
      g.setColor(new Color(67, 143, 80)); g.drawLine(x + 2, y + 8, x - 35, y - 7); g.drawLine(x + 3, y + 8, x + 30, y - 15)
    }

    private def drawWorld(g: Graphics2D): Unit = {
      val w = getWidth
      val h = getHeight
      g.setPaint(new GradientPaint(0, 0, new Color(20, 17, 57), 0, 245, new Color(91, 40, 81)))
      g.fillRect(0, 0, w, h)
      drawStars(g, 0, 0, w, 210, 55)
      // Moon and distant skyline
      g.setColor(new Color(255, 223, 143, 32))
      g.fillOval(w - 170, 25, 108, 108)
      g.setColor(new Color(255, 229, 164))
      g.fillOval(w - 146, 49, 60, 60)
      drawSkyline(g, w)

      // Courtyard floor
      g.setPaint(new GradientPaint(0, 205, new Color(88, 76, 111), 0, h, new Color(39, 42, 64)))
      g.fillRect(0, 205, w, h - 205)
      // Perspective paving
      g.setStroke(new BasicStroke(1f))
      var xx = -100
      while (xx < w + 150) {
        g.setColor(new Color(255, 255, 255, 15))
        g.drawLine(xx, 340, xx + 160, h)
        xx += 100
      }
      var yy = 370
      while (yy < h) {
        g.setColor(new Color(255, 255, 255, 14))
        g.drawLine(0, yy, w, yy)
        yy += 48
      }
      g.setColor(new Color(255, 194, 68, 120))
      g.fillRoundRect(0, h - 70, w, 70, 14, 14)
      g.setColor(new Color(58, 42, 66))
      g.fillRect(0, h - 65, w, 65)
      drawRoadDashes(g, w, h)

      drawNormalHouse(g)
      drawSociety(g)
      drawShop(g)
      drawSecretary(g)
      drawPujaArea(g)
      drawDecorationTargets(g)
      drawPlayer(g)
      drawAmbientEffects(g)
    }

    private def drawSkyline(g: Graphics2D, w: Int): Unit = {
      val buildings = Array((0, 145, 78, 120), (80, 125, 90, 140), (180, 150, 65, 112), (250, 130, 90, 132), (700, 135, 86, 128), (800, 155, 66, 108), (875, 120, 100, 143), (970, 148, 80, 116))
      buildings.foreach { b =>
        val (x, y, bw, bh) = b
        g.setColor(new Color(30, 25, 56))
        g.fillRect(x, y, bw, bh)
        g.setColor(new Color(255, 195, 93, 90))
        var wx = x + 10
        while (wx < x + bw - 7) {
          var wy = y + 12
          while (wy < y + bh - 7) {
            if ((wx + wy) % 3 != 0) g.fillRect(wx, wy, 5, 7)
            wy += 19
          }
          wx += 17
        }
      }
    }

    private def drawNormalHouse(g: Graphics2D): Unit = {
      val x = 24; val y = 255; val w = 300; val h = 190
      g.setColor(new Color(15, 12, 27, 90)); g.fillOval(x - 2, y + h - 3, w + 35, 29)
      g.setPaint(new GradientPaint(x, y, new Color(255, 180, 109), x + w, y + h, new Color(142, 69, 89)))
      g.fillRect(x, y + 40, w, h - 40)
      val roof = new Polygon(Array(x - 18, x + w / 2, x + w + 18), Array(y + 48, y - 28, y + 48), 3)
      g.setColor(new Color(95, 44, 73)); g.fillPolygon(roof)
      g.setColor(new Color(255, 218, 149)); g.fillRect(x + 15, y + 56, w - 30, 8)
      // door and windows
      g.setColor(new Color(89, 45, 51)); g.fillRoundRect(x + 128, y + 95, 53, 95, 7, 7)
      g.setColor(new Color(255, 202, 95)); g.fillOval(x + 167, y + 142, 6, 6)
      drawWindow(g, x + 35, y + 87, 52, 47)
      drawWindow(g, x + 215, y + 87, 52, 47)
      g.setFont(font(15, Font.BOLD)); g.setColor(new Color(255, 246, 208))
      centered(g, if (family == "Normal Family") normalFamilyName.take(16).toUpperCase else "NEIGHBOUR'S HOME", x + w / 2, y + 22)
      if (family == "Normal Family") {
        drawStringShadow(g, "HOME", x + 126, y + h + 16, font(12, Font.BOLD), new Color(255, 237, 168))
      }
      drawToran(g, x + 106, y + 84, 106, 20, family == "Normal Family" && targets.exists(_.done))
      if (family == "Normal Family" && targets.exists(t => t.label == "Diyas" && t.done)) {
        drawDiyas(g, x + 35, y + h - 6, 0.75)
        drawDiyas(g, x + 228, y + h - 6, 0.75)
      }
    }

    private def drawSociety(g: Graphics2D): Unit = {
      val x = 347; val y = 205; val w = 365; val h = 249
      g.setColor(new Color(10, 7, 22, 100)); g.fillOval(x - 10, y + h - 2, w + 35, 32)
      g.setPaint(new GradientPaint(x, y, new Color(89, 95, 151), x + w, y + h, new Color(53, 48, 94)))
      g.fillRect(x, y, w, h)
      g.setColor(new Color(255, 198, 109)); g.fillRect(x, y, w, 10)
      g.setColor(new Color(42, 37, 70)); g.fillRect(x + 10, y + h - 45, w - 20, 45)
      // Apartment floors / balconies
      var row = 0
      while (row < 3) {
        var col = 0
        while (col < 4) {
          val wx = x + 22 + col * 83
          val wy = y + 25 + row * 62
          g.setColor(new Color(28, 27, 61)); g.fillRoundRect(wx, wy, 58, 43, 5, 5)
          g.setColor(new Color(255, 221, 135, 235)); g.fillRect(wx + 7, wy + 7, 44, 26)
          g.setColor(new Color(48, 54, 91)); g.fillRect(wx + 7, wy + 7, 44, 5)
          g.setColor(new Color(30, 25, 53)); g.setStroke(new BasicStroke(2f)); g.drawLine(wx + 29, wy + 7, wx + 29, wy + 33)
          g.setColor(new Color(26, 27, 48)); g.drawLine(wx + 4, wy + 35, wx + 54, wy + 35)
          col += 1
        }
        row += 1
      }
      // central door
      g.setColor(new Color(35, 31, 60)); g.fillRoundRect(x + 150, y + 171, 66, 78, 8, 8)
      g.setColor(new Color(255, 197, 90)); g.fillOval(x + 202, y + 211, 7, 7)
      // staircase on the front-left
      g.setColor(new Color(191, 190, 218));
      var i = 0
      while (i < 7) {
        g.fillRect(x + 43 + i * 7, y + 127 + i * 13, 66 - i * 7, 11)
        g.setColor(new Color(100, 94, 143)); g.drawLine(x + 43 + i * 7, y + 138 + i * 13, x + 109, y + 138 + i * 13)
        g.setColor(new Color(191, 190, 218)); i += 1
      }
      g.setColor(new Color(255, 232, 178))
      g.setFont(font(15, Font.BOLD)); centered(g, "GOKULDHAM SOCIETY", x + w / 2, y - 8)
      // fairy lights along roof
      if (family == "Gokuldham Family" && targets.exists(_.done)) drawToran(g, x + 14, y + 8, w - 28, 16, true)
      else drawToran(g, x + 14, y + 8, w - 28, 16, false)
    }

    private def drawShop(g: Graphics2D): Unit = {
      val x = getWidth - 270; val y = 280; val w = 225; val h = 159
      g.setColor(new Color(10, 7, 23, 90)); g.fillOval(x - 8, y + h - 1, w + 22, 23)
      g.setPaint(new GradientPaint(x, y, new Color(94, 47, 85), x + w, y + h, new Color(49, 31, 65)))
      g.fillRect(x, y, w, h)
      val roof = new Polygon(Array(x - 10, x + w / 2, x + w + 10), Array(y + 8, y - 34, y + 8), 3)
      g.setColor(new Color(240, 137, 56)); g.fillPolygon(roof)
      g.setColor(new Color(255, 206, 112)); g.fillRect(x + 12, y + 13, w - 24, 38)
      g.setFont(font(21, Font.BOLD)); g.setColor(new Color(75, 33, 58)); centered(g, "ABDUL'S SHOP", x + w / 2, y + 39)
      g.setColor(new Color(41, 28, 53)); g.fillRoundRect(x + 18, y + 68, 91, 79, 6, 6)
      g.setColor(new Color(255, 191, 70)); g.fillRect(x + 27, y + 77, 73, 9)
      // visible supplies
      drawMiniDiyas(g, x + 29, y + 93)
      g.setColor(new Color(244, 80, 115)); g.fillOval(x + 64, y + 105, 13, 13)
      g.setColor(new Color(89, 224, 175)); g.fillOval(x + 79, y + 105, 13, 13)
      g.setColor(new Color(255, 226, 147)); g.fillRect(x + 126, y + 76, 80, 8)
      g.setColor(new Color(255, 226, 147)); g.fillRoundRect(x + 131, y + 91, 68, 52, 6, 6)
      g.setColor(new Color(64, 47, 76)); g.fillRect(x + 137, y + 97, 56, 39)
      drawStringShadow(g, "Lights", x + 165, y + 116, font(12, Font.BOLD), new Color(255, 219, 121))
      if (supplies) {
        g.setColor(new Color(46, 220, 152)); g.fillOval(x + w - 31, y + 8, 20, 20)
        g.setColor(new Color(13, 65, 48)); g.setFont(font(14, Font.BOLD)); centered(g, "?", x + w - 21, y + 23)
      }
    }

    private def drawSecretary(g: Graphics2D): Unit = {
      val x = 535; val y = 430
      // NPC only emphasized in Gokuldham mission 2
      drawPerson(g, x.toDouble, y.toDouble, new Color(68, 137, 196), new Color(247, 194, 147), false, 0)
      g.setFont(font(11, Font.BOLD)); g.setColor(new Color(250, 245, 220))
      centered(g, "SECRETARY", x, y + 29)
      if (family == "Gokuldham Family" && stage == 1) drawMarker(g, x, y - 47, new Color(93, 233, 180))
    }

    private def drawPujaArea(g: Graphics2D): Unit = {
      val px = if (family == "Gokuldham Family") 535 else 225
      val py = 535
      // small decorated shrine / puja table
      g.setColor(new Color(20, 14, 30, 80)); g.fillOval(px - 48, py + 1, 96, 18)
      g.setColor(new Color(145, 67, 83)); g.fillRoundRect(px - 38, py - 15, 76, 27, 5, 5)
      g.setColor(new Color(255, 188, 70)); g.fillRect(px - 41, py - 20, 82, 8)
      g.setColor(new Color(255, 223, 139)); g.fillOval(px - 22, py - 45, 44, 32)
      g.setColor(new Color(185, 72, 57)); g.fillOval(px - 13, py - 38, 26, 24)
      g.setColor(new Color(255, 221, 117)); g.fillOval(px - 5, py - 31, 10, 10)
      drawMiniDiyas(g, px - 27, py - 4)
      drawMiniDiyas(g, px + 7, py - 4)
      g.setFont(font(11, Font.BOLD)); g.setColor(new Color(255, 242, 201))
      centered(g, if (family == "Gokuldham Family") "COMMUNITY PUJA" else "HOME PUJA", px, py + 31)
      val shouldMark = if (family == "Gokuldham Family") stage == 4 else stage == 2
      if (shouldMark) drawMarker(g, px, py - 65, new Color(255, 210, 77))
      if (pujaDone) centered(g, "SHUBH DEEPAWALI!", px, py - 76)
    }

    private def drawDecorationTargets(g: Graphics2D): Unit = {
      targets.foreach { t =>
        if (t.done) {
          t.label match {
            case "Diyas" => drawDiyas(g, t.x.toInt - 21, t.y.toInt - 5, 0.52)
            case "Flower Toran" => drawToran(g, t.x.toInt - 40, t.y.toInt - 14, 80, 20, true)
            case "Fairy Lights" =>
              var i = 0
              while (i < 6) {
                val lx = t.x.toInt - 28 + i * 11
                g.setColor(new Color(255, 228, 111)); g.fillOval(lx, t.y.toInt - 9 + (i % 2) * 4, 7, 7)
                g.setColor(new Color(255, 228, 111, 55)); g.fillOval(lx - 4, t.y.toInt - 13 + (i % 2) * 4, 15, 15)
                i += 1
              }
            case "Rangoli" => drawRangoli(g, t.x.toInt, t.y.toInt)
            case _ =>
          }
          g.setColor(new Color(96, 245, 174)); g.setFont(font(10, Font.BOLD)); centered(g, "DONE ?", t.x.toInt, t.y.toInt + 24)
        } else if (phase == "playing" && isDecorationStage) {
          val pulse = (math.sin(tick * 0.12) * 4).toInt
          drawMarker(g, t.x.toInt, t.y.toInt - 20 + pulse, new Color(82, 237, 201))
          g.setColor(new Color(236, 255, 248)); g.setFont(font(10, Font.BOLD)); centered(g, t.label.toUpperCase, t.x.toInt, t.y.toInt + 20)
        }
      }
    }

    private def isDecorationStage: Boolean = {
      if (family == "Gokuldham Family") stage == 3 else stage == 1
    }

    private def drawPlayer(g: Graphics2D): Unit = {
      val bob = if (moveLeft || moveRight || moveUp || moveDown) math.sin(walkFrame * 0.65) * 3.0 else math.sin(tick * 0.06) * 1.2
      val x = playerX.toInt
      val y = (playerY + bob).toInt
      g.setColor(new Color(0, 0, 0, 75)); g.fillOval(x - 18, y + 27, 37, 13)
      // legs animate while moving
      g.setStroke(new BasicStroke(6f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
      val swing = if (moveLeft || moveRight || moveUp || moveDown) (math.sin(walkFrame * 0.65) * 4).toInt else 0
      g.setColor(new Color(34, 44, 86)); g.drawLine(x - 5, y + 12, x - 7 + swing, y + 28)
      g.drawLine(x + 5, y + 12, x + 7 - swing, y + 28)
      g.setColor(new Color(255, 198, 93)); g.fillRoundRect(x - 14, y - 2, 28, 24, 9, 9)
      g.setColor(new Color(51, 115, 208)); g.fillRoundRect(x - 13, y - 2, 26, 18, 8, 8)
      g.setColor(new Color(249, 191, 143)); g.fillOval(x - 10, y - 23, 20, 22)
      g.setColor(new Color(56, 34, 30)); g.fillArc(x - 11, y - 26, 22, 16, 0, 180)
      g.setColor(new Color(33, 25, 40)); g.fillOval(x - 5, y - 14, 2, 3); g.fillOval(x + 4, y - 14, 2, 3)
      g.setColor(new Color(126, 55, 43)); g.drawArc(x - 4, y - 10, 8, 5, 200, 140)
      g.setColor(new Color(247, 207, 105)); g.setFont(font(10, Font.BOLD)); centered(g, playerName.take(12).toUpperCase, x, y - 33)
    }

    private def drawAmbientEffects(g: Graphics2D): Unit = {
      // Gentle glowing firefly lights and distant decorative sparkles; purely visual.
      var i = 0
      while (i < 13) {
        val xx = ((i * 97 + tick * (i % 3 + 1)) % math.max(1, getWidth)).toInt
        val yy = 225 + ((i * 41 + tick / 3) % math.max(1, getHeight - 270)).toInt
        val alpha = 70 + ((math.sin(tick * 0.08 + i) + 1) * 60).toInt
        g.setColor(new Color(255, 208, 93, alpha))
        g.fillOval(xx, yy, 3, 3)
        i += 1
      }
      if (tick % 115 < 22) {
        val fx = 140 + (tick * 3 % math.max(1, getWidth - 280))
        drawSparkle(g, fx, 180 + (tick % 23) * 2, new Color(255, 200, 83))
      }
    }

    private def drawHUD(g: Graphics2D): Unit = {
      val w = getWidth
      // top translucent HUD
      g.setColor(new Color(15, 12, 35, 226)); g.fillRoundRect(14, 12, w - 28, 78, 19, 19)
      g.setColor(new Color(255, 194, 72)); g.setStroke(new BasicStroke(1.4f)); g.drawRoundRect(14, 12, w - 28, 78, 19, 19)
      g.setFont(font(20, Font.BOLD)); g.setColor(new Color(255, 212, 104)); g.drawString("BIXUU DIWALI", 31, 40)
      g.setFont(font(12, Font.BOLD)); g.setColor(Color.WHITE); g.drawString(if (family == "Normal Family") normalFamilyName.take(22) else family, 32, 61)
      g.setFont(font(14, Font.BOLD)); g.setColor(new Color(140, 250, 206)); g.drawString("SCORE: " + score, 32, 79)
      g.setFont(font(14, Font.BOLD)); g.setColor(new Color(255, 243, 213)); g.drawString("TASK: " + currentTask, 260, 42)
      g.setFont(font(12, Font.PLAIN)); g.setColor(new Color(224, 218, 241))
      g.drawString("Supplies: " + (if (supplies) "COLLECTED ?" else "NOT COLLECTED") + "     Decorations: " + targets.count(_.done) + "/4", 260, 66)
      g.setColor(new Color(255, 206, 105)); g.setFont(font(11, Font.BOLD)); g.drawString("[E] INTERACT    [ESC] MENU    [P] PAUSE", w - 290, 79)

      // Context interaction prompt
      val prompt = activePrompt
      if (prompt.nonEmpty && phase == "playing") {
        val p = prompt.get
        val tw = math.min(w - 40, 370)
        g.setColor(new Color(9, 12, 26, 220)); g.fillRoundRect((w - tw) / 2, getHeight - 112, tw, 35, 13, 13)
        g.setColor(new Color(255, 212, 101)); g.drawRoundRect((w - tw) / 2, getHeight - 112, tw, 35, 13, 13)
        g.setFont(font(14, Font.BOLD)); g.setColor(Color.WHITE); centered(g, "Press E / ENTER: " + p, w / 2, getHeight - 90)
      }
      // toast at bottom
      if (toastTicks > 0 && toast != null && toast.nonEmpty) {
        g.setFont(font(13, Font.BOLD))
        val fm = g.getFontMetrics
        val tw = math.min(w - 40, fm.stringWidth(toast) + 38)
        g.setColor(new Color(20, 15, 42, 225)); g.fillRoundRect((w - tw) / 2, getHeight - 65, tw, 31, 12, 12)
        g.setColor(new Color(255, 198, 78)); g.drawRoundRect((w - tw) / 2, getHeight - 65, tw, 31, 12, 12)
        g.setColor(new Color(255, 244, 220)); centered(g, toast, w / 2, getHeight - 45)
      }
      // minimap-like mission route at lower left
      g.setColor(new Color(17, 14, 34, 200)); g.fillRoundRect(17, getHeight - 127, 178, 47, 12, 12)
      g.setFont(font(10, Font.BOLD)); g.setColor(new Color(255, 214, 111)); g.drawString("DIWALI CHECKLIST", 28, getHeight - 109)
      g.setFont(font(10, Font.PLAIN)); g.setColor(new Color(228, 229, 245))
      g.drawString((if (supplies) "?" else "?") + " Supplies   " + (if (targets.forall(_.done)) "?" else "?") + " Decorate", 28, getHeight - 94)
      g.drawString((if (pujaDone) "?" else "?") + " Puja", 28, getHeight - 82)
    }

    private def activePrompt: Option[String] = {
      if (family == "Gokuldham Family") {
        stage match {
          case 0 if distance(playerX, playerY, 420, 355) < 95 => Some("Go down stairs")
          case 1 if distance(playerX, playerY, 535, 430) < 85 => Some("Ask permission")
          case 2 if distance(playerX, playerY, getWidth - 175, 420) < 100 => Some("Talk to Abdul")
          case 3 => targets.find(t => !t.done && distance(playerX, playerY, t.x, t.y) < 78).map(_.label)
          case 4 if distance(playerX, playerY, 535, 535) < 90 => Some("Start community puja")
          case _ => None
        }
      } else {
        stage match {
          case 0 if distance(playerX, playerY, getWidth - 175, 420) < 100 => Some("Buy supplies")
          case 1 => targets.find(t => !t.done && distance(playerX, playerY, t.x, t.y) < 78).map(_.label)
          case 2 if distance(playerX, playerY, 225, 535) < 95 => Some("Start home puja")
          case _ => None
        }
      }
    }

    private def drawComplete(g: Graphics2D): Unit = {
      val w = getWidth; val h = getHeight
      g.setColor(new Color(11, 8, 28, 210)); g.fillRect(0, 0, w, h)
      drawStars(g, 0, 0, w, h, 55)
      g.setColor(new Color(255, 202, 72, 50)); g.fillOval(w / 2 - 200, h / 2 - 180, 400, 320)
      g.setPaint(new GradientPaint(w / 2 - 220, h / 2 - 130, new Color(75, 43, 91), w / 2 + 220, h / 2 + 150, new Color(33, 27, 61)))
      g.fillRoundRect(w / 2 - 265, h / 2 - 160, 530, 315, 34, 34)
      g.setStroke(new BasicStroke(3f)); g.setColor(new Color(255, 205, 82)); g.drawRoundRect(w / 2 - 265, h / 2 - 160, 530, 315, 34, 34)
      drawDiyas(g, w / 2 - 180, h / 2 + 36, 1.0)
      drawDiyas(g, w / 2 + 125, h / 2 + 36, 1.0)
      g.setFont(font(36, Font.BOLD)); g.setColor(new Color(255, 208, 80)); centered(g, "SHUBH DEEPAWALI!", w / 2, h / 2 - 89)
      g.setFont(font(21, Font.BOLD)); g.setColor(Color.WHITE); centered(g, "MISSION COMPLETED", w / 2, h / 2 - 48)
      g.setFont(font(18, Font.PLAIN)); g.setColor(new Color(226, 222, 243)); centered(g, family + " ? All tasks finished", w / 2, h / 2 - 12)
      g.setFont(font(24, Font.BOLD)); g.setColor(new Color(121, 248, 201)); centered(g, "FINAL SCORE: " + score, w / 2, h / 2 + 29)
      g.setFont(font(15, Font.BOLD)); g.setColor(new Color(255, 222, 139)); centered(g, "Press ENTER to play again  ?  ESC for family selection", w / 2, h / 2 + 99)
      // Celebratory virtual sparkles
      var i = 0
      while (i < 14) {
        val fx = (i * 83 + tick * 2) % math.max(1, w)
        val fy = (i * 57 + tick) % math.max(1, h)
        drawSparkle(g, fx, fy, if (i % 2 == 0) new Color(255, 206, 91) else new Color(101, 244, 215))
        i += 1
      }
    }

    private def drawPause(g: Graphics2D): Unit = {
      g.setColor(new Color(9, 7, 22, 195)); g.fillRect(0, 0, getWidth, getHeight)
      g.setFont(font(34, Font.BOLD)); g.setColor(new Color(255, 211, 99)); centered(g, "PAUSED", getWidth / 2, getHeight / 2 - 10)
      g.setFont(font(16, Font.BOLD)); g.setColor(Color.WHITE); centered(g, "Press P or E to continue", getWidth / 2, getHeight / 2 + 28)
    }

    private def drawWindow(g: Graphics2D, x: Int, y: Int, w: Int, h: Int): Unit = {
      g.setColor(new Color(59, 46, 78)); g.fillRoundRect(x, y, w, h, 5, 5)
      g.setColor(new Color(255, 226, 153)); g.fillRect(x + 5, y + 5, w - 10, h - 10)
      g.setColor(new Color(72, 57, 85)); g.setStroke(new BasicStroke(3f)); g.drawLine(x + w / 2, y + 4, x + w / 2, y + h - 4)
      g.drawLine(x + 4, y + h / 2, x + w - 4, y + h / 2)
    }

    private def drawToran(g: Graphics2D, x: Int, y: Int, width: Int, height: Int, bright: Boolean): Unit = {
      g.setColor(new Color(40, 26, 44)); g.setStroke(new BasicStroke(2f)); g.drawArc(x, y - 2, width, height * 2, 180, 180)
      var i = 0
      while (i <= 9) {
        val xx = x + i * width / 9
        val yy = y + height / 2 + (math.sin(i * math.Pi / 9) * height * 0.55).toInt
        g.setColor(if (i % 2 == 0) new Color(255, 183, 65) else new Color(64, 205, 142))
        g.fillOval(xx - 4, yy - 3, 8, 10)
        if (bright) {
          g.setColor(new Color(255, 222, 103, 70)); g.fillOval(xx - 9, yy - 8, 18, 18)
        }
        i += 1
      }
      if (bright) {
        g.setColor(new Color(255, 224, 143)); g.setStroke(new BasicStroke(3f)); g.drawArc(x, y - 2, width, height * 2, 180, 180)
      }
    }

    private def drawDiyas(g: Graphics2D, x: Int, y: Int, scale: Double): Unit = {
      var i = 0
      while (i < 3) {
        val xx = x + (i * 25 * scale).toInt
        val yy = y + (if (i == 1) -4 else 0)
        g.setColor(new Color(255, 190, 58, 45)); g.fillOval(xx - 8, yy - 12, (32 * scale).toInt, (28 * scale).toInt)
        g.setColor(new Color(204, 91, 40)); g.fillOval(xx, yy, (21 * scale).toInt, (10 * scale).toInt)
        g.setColor(new Color(255, 207, 82)); g.fillOval(xx + (5 * scale).toInt, yy - (5 * scale).toInt, (11 * scale).toInt, (7 * scale).toInt)
        g.setColor(new Color(255, 249, 191)); g.fillOval(xx + (9 * scale).toInt, yy - (11 * scale).toInt, (4 * scale).toInt, (8 * scale).toInt)
        i += 1
      }
    }

    private def drawMiniDiyas(g: Graphics2D, x: Int, y: Int): Unit = {
      var i = 0
      while (i < 3) {
        val xx = x + i * 18
        g.setColor(new Color(241, 128, 49)); g.fillOval(xx, y, 13, 7)
        g.setColor(new Color(255, 230, 125)); g.fillOval(xx + 4, y - 5, 5, 6)
        i += 1
      }
    }

    private def drawRangoli(g: Graphics2D, x: Int, y: Int): Unit = {
      val colors = Array(new Color(244, 86, 115), new Color(255, 205, 72), new Color(77, 212, 172), new Color(127, 115, 255))
      g.setColor(new Color(255, 231, 151, 40)); g.fillOval(x - 27, y - 13, 54, 28)
      var i = 0
      while (i < 8) {
        val angle = i * math.Pi / 4.0
        val cx = x + (math.cos(angle) * 17).toInt
        val cy = y + (math.sin(angle) * 8).toInt
        g.setColor(colors(i % colors.length)); g.fillOval(cx - 5, cy - 4, 10, 8)
        i += 1
      }
      g.setColor(new Color(255, 244, 193)); g.fillOval(x - 4, y - 3, 8, 6)
    }

    private def drawMarker(g: Graphics2D, x: Int, y: Int, c: Color): Unit = {
      val pulse = (math.sin(tick * 0.13) * 3).toInt
      g.setColor(new Color(c.getRed, c.getGreen, c.getBlue, 45)); g.fillOval(x - 15 - pulse, y - 15 - pulse, 30 + pulse * 2, 30 + pulse * 2)
      g.setColor(c); g.setStroke(new BasicStroke(2f)); g.drawOval(x - 8, y - 8, 16, 16)
      g.fillOval(x - 3, y - 3, 6, 6)
    }

    private def drawPerson(g: Graphics2D, xx: Double, yy: Double, shirt: Color, skin: Color, moving: Boolean, frame: Int): Unit = {
      val x = xx.toInt; val y = yy.toInt
      g.setColor(new Color(0, 0, 0, 65)); g.fillOval(x - 15, y + 19, 31, 10)
      g.setColor(new Color(35, 36, 66)); g.setStroke(new BasicStroke(5f)); g.drawLine(x - 4, y + 8, x - 7, y + 21); g.drawLine(x + 4, y + 8, x + 7, y + 21)
      g.setColor(shirt); g.fillRoundRect(x - 13, y - 7, 26, 19, 7, 7)
      g.setColor(skin); g.fillOval(x - 9, y - 25, 18, 19)
      g.setColor(new Color(49, 30, 28)); g.fillArc(x - 10, y - 28, 20, 14, 0, 180)
    }

    private def drawMiniBuilding(g: Graphics2D, x: Int, y: Int, w: Int, h: Int): Unit = {
      g.setColor(new Color(76, 69, 132)); g.fillRect(x, y, w, h)
      g.setColor(new Color(255, 205, 103)); g.fillRect(x, y, w, 6)
      var row = 0
      while (row < 2) {
        var col = 0
        while (col < 3) {
          g.setColor(new Color(255, 226, 153)); g.fillRect(x + 10 + col * 27, y + 13 + row * 23, 16, 14)
          col += 1
        }
        row += 1
      }
    }

    private def drawMiniHouse(g: Graphics2D, x: Int, y: Int, w: Int, h: Int): Unit = {
      val roof = new Polygon(Array(x - 5, x + w / 2, x + w + 5), Array(y + 22, y - 5, y + 22), 3)
      g.setColor(new Color(220, 117, 72)); g.fillPolygon(roof)
      g.setColor(new Color(249, 191, 128)); g.fillRect(x + 4, y + 20, w - 8, h - 20)
      g.setColor(new Color(88, 48, 62)); g.fillRect(x + w / 2 - 10, y + 38, 20, h - 38)
      g.setColor(new Color(255, 225, 153)); g.fillRect(x + 13, y + 31, 17, 16); g.fillRect(x + w - 30, y + 31, 17, 16)
    }

    private def drawRoadDashes(g: Graphics2D, w: Int, h: Int): Unit = {
      g.setColor(new Color(255, 224, 133, 100))
      var x = 15
      while (x < w) { g.fillRoundRect(x, h - 42, 34, 3, 4, 4); x += 65 }
    }

    private def drawSparkle(g: Graphics2D, x: Int, y: Int, c: Color): Unit = {
      g.setColor(new Color(c.getRed, c.getGreen, c.getBlue, 70)); g.fillOval(x - 9, y - 9, 18, 18)
      g.setColor(c); g.setStroke(new BasicStroke(1.5f)); g.drawLine(x - 6, y, x + 6, y); g.drawLine(x, y - 6, x, y + 6)
      g.fillOval(x - 2, y - 2, 4, 4)
    }

    private def drawStars(g: Graphics2D, x: Int, y: Int, w: Int, h: Int, count: Int): Unit = {
      var i = 0
      while (i < count) {
        val sx = x + ((i * 137 + 31) % math.max(1, w))
        val sy = y + ((i * 67 + 13) % math.max(1, h))
        val a = 70 + ((i * 29) % 160)
        g.setColor(new Color(255, 239, 199, a))
        val s = if (i % 7 == 0) 3 else 2
        g.fillOval(sx, sy, s, s)
        i += 1
      }
    }

    private def centered(g: Graphics2D, text: String, x: Int, y: Int): Unit = {
      val fm = g.getFontMetrics
      g.drawString(text, x - fm.stringWidth(text) / 2, y)
    }

    private def drawStringShadow(g: Graphics2D, text: String, x: Int, y: Int, f: Font, c: Color): Unit = {
      g.setFont(f); g.setColor(new Color(0, 0, 0, 125)); g.drawString(text, x + 1, y + 1)
      g.setColor(c); g.drawString(text, x, y)
    }

    def clearHeldKeys(): Unit = {
      heldKeys.clear()
      moveUp = false
      moveDown = false
      moveLeft = false
      moveRight = false
    }

    override def mouseClicked(e: MouseEvent): Unit = {
      if (phase == "menu") {
        if (menuGokul.contains(e.getPoint)) startGame("Gokuldham Family")
        else if (menuNormal.contains(e.getPoint)) startGame("Normal Family")
      }
      requestFocusInWindow()
      if (getTopLevelAncestor != null) getTopLevelAncestor.requestFocus()
    }
    override def mousePressed(e: MouseEvent): Unit = {
      requestFocusInWindow()
    }
    override def mouseReleased(e: MouseEvent): Unit = {}
    override def mouseEntered(e: MouseEvent): Unit = {}
    override def mouseExited(e: MouseEvent): Unit = {}
  }
}



// ===== INTEGRATED GAME MODULE: BixuuCardsBattle_ActionArena.scala =====

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.DISPOSE_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()
  }})
}


// Start the BIXUU launcher when this complete source is run in Kojo.

BixuuAllGamesHub.main(new Array[String](0))