Code Sketch


Bixuu multiplayer game steal an egg
By: Mhalsakant School
Category: Programming
import java.awt.{BasicStroke, BorderLayout, CardLayout, Color, Dimension, Font, GradientPaint, Graphics, Graphics2D, GridLayout, Polygon, RenderingHints}
import java.awt.event.{ActionEvent, ActionListener, KeyEvent, WindowAdapter, WindowEvent}
import java.io.{File, FileInputStream, FileOutputStream}
import java.util.Properties
import javax.swing.{AbstractAction, BorderFactory, JButton, JComponent, JDialog, JFrame, JLabel, JOptionPane, JPanel, JScrollPane, JPasswordField, JTextField, KeyStroke, SwingConstants, SwingUtilities, Timer, WindowConstants}
import javax.swing.border.EmptyBorder
import scala.collection.mutable.ArrayBuffer
import scala.util.Random

object SpringSeasonCatchTheEgg {
  private val TITLE = "SPRING SEASON - CATCH THE EGG 2.5D"
  private val ADMIN_PASSWORD = "BIXUU@SSCTE"
  private val MAX_SPEED = 1000000
  private val rng = new Random()

  private case class Biome(
    name: String,
    gate: Int,
    sky: Color,
    ground: Color,
    accent: Color,
    guardian: String,
    guardianColor: Color
  )

  private val biomes = Vector(
    Biome("HEN FOREST", 0, new Color(139,210,245), new Color(81,157,78), new Color(250,220,100), "CHICKEN", new Color(238,190,65)),
    Biome("TIGER JUNGLE", 20, new Color(115,190,145), new Color(39,112,58), new Color(244,155,55), "TIGER", new Color(231,135,40)),
    Biome("LAKE", 40, new Color(117,207,235), new Color(45,137,165), new Color(220,250,255), "SWAN", Color.WHITE),
    Biome("DESERT", 60, new Color(249,211,139), new Color(190,145,78), new Color(255,232,150), "SCORPION", new Color(166,105,48)),
    Biome("ARCTIC", 80, new Color(211,238,250), new Color(134,184,210), Color.WHITE, "POLAR FOX", new Color(225,230,236)),
    Biome("WATER", 100, new Color(78,163,215), new Color(26,96,154), new Color(155,226,255), "SHARK", new Color(103,132,154)),
    Biome("DRAGON PEAK", 100, new Color(151,120,173), new Color(97,67,96), new Color(255,126,91), "DRAGON", new Color(211,81,72)),
    Biome("CAVE", 120, new Color(100,108,119), new Color(52,58,69), new Color(185,195,208), "BAT", new Color(65,67,88)),
    Biome("VOLCANO", 140, new Color(110,63,54), new Color(77,35,30), new Color(255,111,47), "LAVA LIZARD", new Color(207,91,49)),
    Biome("ABYSS OCEAN", 160, new Color(34,65,113), new Color(10,27,69), new Color(85,190,230), "OCTOPUS", new Color(158,94,186)),
    Biome("PREHISTORIC", 180, new Color(173,187,119), new Color(92,111,53), new Color(206,171,105), "DINO", new Color(89,139,75)),
    Biome("HEAVEN", 200, new Color(218,235,255), new Color(165,197,229), Color.WHITE, "PEGASUS", new Color(246,246,255)),
    Biome("COSMIC", 220, new Color(68,57,128), new Color(29,23,72), new Color(184,135,255), "SPACE CAT", new Color(164,134,242)),
    Biome("TOY BOX", 240, new Color(255,193,211), new Color(126,188,232), new Color(255,237,115), "TOY ROBOT", new Color(223,91,108)),
    Biome("DINO VALLEY", 260, new Color(158,183,107), new Color(81,120,55), new Color(219,185,119), "RAPTOR", new Color(97,144,69)),
    Biome("SKY CASTLE", 280, new Color(146,202,242), new Color(106,153,207), Color.WHITE, "SKY BIRD", new Color(244,244,250)),
    Biome("DISCO", 300, new Color(103,71,156), new Color(45,35,94), new Color(255,105,214), "DISCO FOX", new Color(217,100,200)),
    Biome("CANDY KINGDOM", 320, new Color(255,190,216), new Color(233,141,181), new Color(255,245,160), "CANDY BEAR", new Color(220,128,158)),
    Biome("CRYSTAL CAVES", 340, new Color(122,113,194), new Color(67,63,132), new Color(176,239,255), "CRYSTAL GOLEM", new Color(116,203,219)),
    Biome("SKELETON MARSH", 360, new Color(114,134,100), new Color(50,72,54), new Color(207,218,167), "MARSH TURTLE", new Color(106,135,92)),
    Biome("RAINBOW REALM", 400, new Color(147,196,245), new Color(97,160,132), Color.WHITE, "RAINBOW LION", new Color(226,151,75)),
    Biome("APPLE ORCHARD", 420, new Color(164,218,247), new Color(88,151,77), new Color(220,48,55), "APPLE BEAR", new Color(153,91,61)),
    Biome("STEEL FACTORY", 500, new Color(112,132,149), new Color(58,70,83), new Color(255,184,84), "STEEL BOT", new Color(118,140,154))
  )

  private val saveDir = new File(System.getProperty("user.home"), ".spring-season-catch-egg")
  private val props = new Properties()

  private var playerId = ""
  private var playerName = "Player"
  private var player2Name = "Player 2"
  private var multiplayerMode = false
  private var coins = 0L
  private var speed = 0
  private var eggsDelivered = 0L
  private var pets = 0
  private var selectedBiome = 0
  private var eventActive = true
  private var frame: JFrame = _
  private var cards: CardLayout = _
  private var root: JPanel = _
  private var gamePanel: GamePanel = _
  private var statsLabel: JLabel = _
  private var modeWelcomeLabel: JLabel = _
  private var statsTimer: Timer = _

  def main(args: Array[String]): Unit = {
    if (java.awt.GraphicsEnvironment.isHeadless) {
      System.err.println("A desktop display is required. Run this in Kojo on your computer.")
      return
    }
    SwingUtilities.invokeLater(new Runnable {
      override def run(): Unit = {
        try {
          createWindow()
        } catch {
          case ex: Throwable =>
            ex.printStackTrace()
            JOptionPane.showMessageDialog(null, "Startup error: " + ex.toString, TITLE, JOptionPane.ERROR_MESSAGE)
        }
      }
    })
  }

  private def profileFile: File = {
    val safeId = playerId.trim.toLowerCase(java.util.Locale.ROOT)
    new File(saveDir, safeId + ".properties")
  }

  // Every new Player ID starts with zero progress. Returning IDs load only their own file.
  private def loadProfile(id: String, enteredName: String): Unit = {
    playerId = id.trim
    playerName = enteredName.trim.take(20)
    coins = 0L
    speed = 0
    eggsDelivered = 0L
    pets = 0
    selectedBiome = 0
    eventActive = true
    props.clear()

    val file = profileFile
    if (file.isFile) {
      var in: FileInputStream = null
      try {
        in = new FileInputStream(file)
        props.load(in)
        playerName = props.getProperty("playerName", playerName)
        coins = readLong("coins", 0L)
        speed = math.max(0, math.min(MAX_SPEED, readInt("speed", 0)))
        eggsDelivered = readLong("eggsDelivered", 0L)
        pets = math.max(0, readInt("pets", 0))
        selectedBiome = math.max(0, math.min(biomes.length - 1, readInt("selectedBiome", 0)))
      } catch {
        case ex: Exception =>
          System.err.println("Profile load warning: " + ex.toString)
          coins = 0L
          speed = 0
          eggsDelivered = 0L
          pets = 0
          selectedBiome = 0
      } finally {
        if (in != null) in.close()
      }
    } else {
      // Brand-new account: all values remain zero.
      saveGame()
    }
  }

  private def readLong(key: String, defaultValue: Long): Long = {
    try props.getProperty(key, defaultValue.toString).toLong
    catch { case _: Exception => defaultValue }
  }

  private def readInt(key: String, defaultValue: Int): Int = {
    try props.getProperty(key, defaultValue.toString).toInt
    catch { case _: Exception => defaultValue }
  }

  private def saveGame(): Unit = {
    if (playerId.trim.isEmpty) return
    var out: FileOutputStream = null
    try {
      if (!saveDir.exists()) saveDir.mkdirs()
      props.setProperty("playerId", playerId)
      props.setProperty("playerName", playerName)
      props.setProperty("coins", coins.toString)
      props.setProperty("speed", speed.toString)
      props.setProperty("eggsDelivered", eggsDelivered.toString)
      props.setProperty("pets", pets.toString)
      props.setProperty("selectedBiome", selectedBiome.toString)
      out = new FileOutputStream(profileFile)
      props.store(out, TITLE)
    } catch {
      case ex: Exception => System.err.println("Save warning: " + ex.toString)
    } finally {
      if (out != null) out.close()
    }
  }

  private def createWindow(): Unit = {
    frame = new JFrame(TITLE)
    frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE)
    frame.setSize(1180, 780)
    frame.setMinimumSize(new Dimension(900, 620))
    frame.setLocationRelativeTo(null)
    cards = new CardLayout()
    root = new JPanel(cards)
    root.add(createLoginScreen(), "login")
    root.add(createModeScreen(), "mode")
    root.add(new JPanel(), "game")
    frame.setContentPane(root)
    frame.addWindowListener(new WindowAdapter {
      override def windowClosing(e: WindowEvent): Unit = {
        saveGame()
        if (statsTimer != null) statsTimer.stop()
        if (gamePanel != null) gamePanel.stopGame()
        frame.dispose()
        System.exit(0)
      }
    })
    cards.show(root, "login")
    frame.setVisible(true)
    frame.toFront()
  }

  private def createLoginScreen(): JPanel = {
    val background = new JPanel(new BorderLayout())
    background.setBackground(new Color(30, 105, 73))
    background.setBorder(new EmptyBorder(35, 45, 35, 45))
    val box = new JPanel()
    box.setLayout(new javax.swing.BoxLayout(box, javax.swing.BoxLayout.Y_AXIS))
    box.setBackground(new Color(255, 250, 226))
    box.setBorder(new EmptyBorder(35, 50, 35, 50))

    val title = new JLabel("SPRING SEASON")
    title.setForeground(new Color(35, 130, 78))
    title.setFont(new Font("Arial", Font.BOLD, 40))
    title.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)
    val subtitle = new JLabel("CATCH THE EGG - 2.5D ADVENTURE")
    subtitle.setFont(new Font("Arial", Font.BOLD, 22))
    subtitle.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)
    val idPrompt = new JLabel("Create or enter your Player ID (3-24 letters/numbers)")
    idPrompt.setFont(new Font("Arial", Font.BOLD, 15))
    idPrompt.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)
    val idField = new JTextField("", 20)
    idField.setMaximumSize(new Dimension(310, 40))
    idField.setFont(new Font("Arial", Font.PLAIN, 17))
    val prompt = new JLabel("Enter your player name")
    prompt.setFont(new Font("Arial", Font.BOLD, 17))
    prompt.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)
    val nameField = new JTextField(playerName, 20)
    nameField.setMaximumSize(new Dimension(310, 42))
    nameField.setFont(new Font("Arial", Font.PLAIN, 18))
    val play = new JButton("SIGN IN AND PLAY")
    play.setFont(new Font("Arial", Font.BOLD, 19))
    play.setBackground(new Color(54, 164, 94))
    play.setForeground(Color.WHITE)
    play.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)
    val hint = new JLabel("Each new Player ID starts at ZERO ? Steal eggs ? Escape to base ? Hatch pets")
    hint.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)

    val action = new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        val id = idField.getText.trim
        val text = nameField.getText.trim
        if (!id.matches("[A-Za-z0-9_-]{3,24}")) {
          JOptionPane.showMessageDialog(frame, "Player ID must be 3-24 characters: letters, numbers, _ or -.")
        } else if (text.isEmpty) {
          JOptionPane.showMessageDialog(frame, "Please enter your player name.")
        } else {
          loadProfile(id, text)
          if (modeWelcomeLabel != null) modeWelcomeLabel.setText("Welcome, " + playerName)
          JOptionPane.showMessageDialog(
            frame,
            "Welcome, " + playerName + "!\nPlayer ID: " + playerId +
              "\nBixuBucks: " + coins + "\nSpeed: " + speed + "\nPets: " + pets,
            "PLAYER PROFILE",
            JOptionPane.INFORMATION_MESSAGE
          )
          cards.show(root, "mode")
        }
      }
    }
    play.addActionListener(action)
    idField.addActionListener(action)
    nameField.addActionListener(action)
    box.add(title)
    box.add(javax.swing.Box.createVerticalStrut(10))
    box.add(subtitle)
    box.add(javax.swing.Box.createVerticalStrut(32))
    box.add(idPrompt)
    box.add(javax.swing.Box.createVerticalStrut(8))
    box.add(idField)
    box.add(javax.swing.Box.createVerticalStrut(15))
    box.add(prompt)
    box.add(javax.swing.Box.createVerticalStrut(10))
    box.add(nameField)
    box.add(javax.swing.Box.createVerticalStrut(22))
    box.add(play)
    box.add(javax.swing.Box.createVerticalStrut(25))
    box.add(hint)
    background.add(box, BorderLayout.CENTER)
    background
  }

  private def createModeScreen(): JPanel = {
    val panel = new JPanel(new GridLayout(0, 1, 14, 14))
    panel.setBackground(new Color(22, 39, 64))
    panel.setBorder(new EmptyBorder(65, 190, 65, 190))

    val title = new JLabel("CHOOSE YOUR GAME MODE", SwingConstants.CENTER)
    title.setForeground(new Color(255, 215, 110))
    title.setFont(new Font("Arial", Font.BOLD, 32))

    modeWelcomeLabel = new JLabel("Welcome, " + playerName, SwingConstants.CENTER)
    modeWelcomeLabel.setForeground(Color.WHITE)
    modeWelcomeLabel.setFont(new Font("Arial", Font.BOLD, 19))

    val solo = new JButton("SOLO PLAY")
    solo.setFont(new Font("Arial", Font.BOLD, 21))
    solo.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        multiplayerMode = false
        openGame()
      }
    })

    val multi = new JButton("MULTIPLAYER - 2 PLAYERS (SAME COMPUTER)")
    multi.setFont(new Font("Arial", Font.BOLD, 19))
    multi.setBackground(new Color(55, 145, 105))
    multi.setForeground(Color.WHITE)
    multi.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        val entered = JOptionPane.showInputDialog(
          frame, "Enter Player 2 name:", "Player 2"
        )
        player2Name = if (entered == null || entered.trim.isEmpty) "Player 2" else entered.trim.take(20)
        multiplayerMode = true
        openGame()
      }
    })

    val note = new JLabel(
      "Player 1: W A S D + E     |     Player 2: ARROW KEYS + ENTER",
      SwingConstants.CENTER
    )
    note.setForeground(new Color(210, 224, 240))
    note.setFont(new Font("Arial", Font.PLAIN, 13))

    val onlineNote = new JLabel(
      "Local co-op: both players use the same keyboard/computer. Not online multiplayer.",
      SwingConstants.CENTER
    )
    onlineNote.setForeground(new Color(255, 211, 145))
    onlineNote.setFont(new Font("Arial", Font.PLAIN, 12))

    val back = new JButton("BACK TO LOGIN")
    back.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = cards.show(root, "login")
    })

    panel.add(title)
    panel.add(modeWelcomeLabel)
    panel.add(solo)
    panel.add(multi)
    panel.add(note)
    panel.add(onlineNote)
    panel.add(back)
    panel
  }

  private def openGame(): Unit = {
    if (gamePanel != null) gamePanel.stopGame()
    if (statsTimer != null) statsTimer.stop()
    // Login and mode cards stay at indices 0 and 1; replace the old game card.
    if (root.getComponentCount > 2) root.remove(2)
    val screen = new JPanel(new BorderLayout())
    screen.add(createToolbar(), BorderLayout.NORTH)
    gamePanel = new GamePanel()
    screen.add(gamePanel, BorderLayout.CENTER)
    root.add(screen, "game")
    root.revalidate()
    root.repaint()
    cards.show(root, "game")
    gamePanel.startGame()
  }

  private def statsText(): String = {
    val modeText = if (multiplayerMode) "MULTIPLAYER: " + player2Name else "SOLO"
    "BixuBucks: " + coins + "   Speed: " + speed + "   Eggs: " + eggsDelivered + "   Pets: " + pets + "   Biome: " + biomes(selectedBiome).name + "   " + modeText
  }

  private def createToolbar(): JPanel = {
    val bar = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.CENTER, 8, 8))
    bar.setBackground(new Color(25, 43, 66))
    val name = new JLabel("PLAYER: " + playerName + "  [ID: " + playerId + "]")
    name.setForeground(Color.WHITE)
    name.setFont(new Font("Arial", Font.BOLD, 13))
    statsLabel = new JLabel(statsText())
    statsLabel.setForeground(new Color(255, 225, 120))
    statsLabel.setFont(new Font("Arial", Font.BOLD, 13))

    val biomesButton = new JButton("BIOMES [M]")
    biomesButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showBiomes(false)
    })
    val saveButton = new JButton("SAVE")
    saveButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        saveGame()
        JOptionPane.showMessageDialog(frame, "Game saved.")
      }
    })
    val adminButton = new JButton("ADMIN PANEL")
    adminButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = openAdminPanel()
    })
    val helpButton = new JButton("HOW TO PLAY")
    val modeButton = new JButton("GAME MODES")
    modeButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        saveGame()
        if (gamePanel != null) gamePanel.stopGame()
        if (statsTimer != null) statsTimer.stop()
        cards.show(root, "mode")
      }
    })
    val switchButton = new JButton("SWITCH ID")
    switchButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        saveGame()
        if (gamePanel != null) gamePanel.stopGame()
        if (statsTimer != null) statsTimer.stop()
        cardLayoutSafeShowLogin()
      }
    })
    helpButton.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = showHelp()
    })

    statsTimer = new Timer(300, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = {
        if (statsLabel != null) statsLabel.setText(statsText())
      }
    })
    statsTimer.start()
    bar.add(name)
    bar.add(statsLabel)
    bar.add(biomesButton)
    bar.add(saveButton)
    bar.add(adminButton)
    bar.add(helpButton)
    bar.add(modeButton)
    bar.add(switchButton)
    bar
  }

  private def cardLayoutSafeShowLogin(): Unit = {
    if (cards != null && root != null) {
      cards.show(root, "login")
      frame.toFront()
    }
  }

  private def showHelp(): Unit = {
    JOptionPane.showMessageDialog(
      frame,
      "SOLO: W / A / S / D = Move, E = Pick up/deliver egg.\nMULTIPLAYER: Player 1 uses W/A/S/D + E. Player 2 uses ARROW KEYS + ENTER.\nT = Train on the treadmill (stand on it and hold T). M = Open biomes.\n\nMultiplayer is local co-op on the same computer, not online. Both players share the current Player ID's coins, speed and pets. Every new Player ID starts with 0 BixuBucks, 0 Speed, 0 Pets and 0 Eggs. Steal an egg from the guardian nest, escape the guardian, return to your base and deliver it. Your pets earn BixuBucks over time.",
      "HOW TO PLAY",
      JOptionPane.INFORMATION_MESSAGE
    )
  }

  private def showBiomes(adminUnlock: Boolean): Unit = {
    val dialog = new JDialog(frame, "BIOME DISCOVERY - 23 AREAS", true)
    val outer = new JPanel(new BorderLayout(8, 8))
    outer.setBackground(new Color(12, 21, 40))
    outer.setBorder(new EmptyBorder(14, 14, 14, 14))
    val heading = new JLabel("BIOME DISCOVERY   |   YOUR SPEED: " + speed)
    heading.setForeground(new Color(255, 215, 112))
    heading.setFont(new Font("Arial", Font.BOLD, 21))
    outer.add(heading, BorderLayout.NORTH)

    val list = new JPanel(new GridLayout(0, 2, 9, 9))
    list.setBackground(new Color(12, 21, 40))
    var i = 0
    while (i < biomes.length) {
      val index = i
      val b = biomes(index)
      val unlocked = adminUnlock || index == 0 || speed >= b.gate
      val card = new JPanel(new BorderLayout(5, 5))
      card.setBackground(new Color(28, 42, 65))
      card.setBorder(BorderFactory.createCompoundBorder(
        BorderFactory.createLineBorder(if (selectedBiome == index) b.accent else new Color(78, 102, 132), 2),
        new EmptyBorder(9, 10, 9, 10)
      ))
      val label = new JLabel(f"${index + 1}%02d  ${b.name}")
      label.setForeground(Color.WHITE)
      label.setFont(new Font("Arial", Font.BOLD, 14))
      val gate = new JLabel("Required Speed: " + b.gate + "   Guardian: " + b.guardian)
      gate.setForeground(new Color(193, 210, 231))
      gate.setFont(new Font("Arial", Font.PLAIN, 11))
      val button = new JButton(if (unlocked) "ENTER BIOME" else "LOCKED - NEED " + (b.gate - speed) + " MORE SPEED")
      button.setEnabled(unlocked)
      button.setBackground(if (unlocked) new Color(54, 145, 102) else new Color(75, 79, 91))
      button.setForeground(Color.WHITE)
      button.addActionListener(new ActionListener {
        override def actionPerformed(e: ActionEvent): Unit = {
          selectedBiome = index
          saveGame()
          if (gamePanel != null) gamePanel.changeBiome()
          dialog.dispose()
        }
      })
      val labels = new JPanel(new GridLayout(2, 1, 2, 2))
      labels.setOpaque(false)
      labels.add(label)
      labels.add(gate)
      card.add(labels, BorderLayout.CENTER)
      card.add(button, BorderLayout.SOUTH)
      list.add(card)
      i += 1
    }
    val scroll = new JScrollPane(list)
    scroll.getVerticalScrollBar.setUnitIncrement(18)
    outer.add(scroll, BorderLayout.CENTER)
    val close = new JButton("CLOSE")
    close.addActionListener(new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = dialog.dispose()
    })
    val footer = new JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT))
    footer.setOpaque(false)
    footer.add(close)
    outer.add(footer, BorderLayout.SOUTH)
    dialog.setContentPane(outer)
    dialog.setSize(820, 640)
    dialog.setLocationRelativeTo(frame)
    dialog.setVisible(true)
  }

  private def openAdminPanel(): Unit = {
    val field = new JPasswordField(18)
    val result = JOptionPane.showConfirmDialog(frame, field, "GAME CREATOR ADMIN LOGIN", JOptionPane.OK_CANCEL_OPTION)
    if (result != JOptionPane.OK_OPTION) return
    if (new String(field.getPassword) != ADMIN_PASSWORD) {
      JOptionPane.showMessageDialog(frame, "Wrong admin password.", "Access denied", JOptionPane.ERROR_MESSAGE)
      return
    }
    val options = Array[AnyRef]("Give BixuBucks / Coins", "Set speed", "Give pets", "Unlock/select any biome", "Start spring event", "Stop spring event", "View stats", "Close")
    var open = true
    while (open) {
      val choice = JOptionPane.showOptionDialog(frame,
        "GAME CREATOR PANEL\nPlayer: " + playerName + "\nPlayer ID: " + playerId + "\nBixuBucks: " + coins + "\nSpeed: " + speed + "\nPets: " + pets,
        "ADMIN PANEL", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options(0))
      choice match {
        case 0 =>
          val text = JOptionPane.showInputDialog(frame, "BixuBucks to add to Player ID " + playerId + ":")
          if (text != null) {
            try {
              val amount = text.trim.toLong
              if (amount > 0L) coins = if (Long.MaxValue - coins < amount) Long.MaxValue else coins + amount
              saveGame()
            } catch { case _: NumberFormatException => JOptionPane.showMessageDialog(frame, "Enter a valid whole number.") }
          }
        case 1 =>
          val text = JOptionPane.showInputDialog(frame, "Set speed from 0 to " + MAX_SPEED + ":")
          if (text != null) {
            try {
              val value = text.trim.toInt
              if (value >= 0 && value <= MAX_SPEED) speed = value
              else JOptionPane.showMessageDialog(frame, "Speed must be between 0 and " + MAX_SPEED)
              saveGame()
            } catch { case _: NumberFormatException => JOptionPane.showMessageDialog(frame, "Enter a valid whole number.") }
          }
        case 2 =>
          val text = JOptionPane.showInputDialog(frame, "Number of pets to add:")
          if (text != null) {
            try {
              val amount = text.trim.toInt
              if (amount >= 0 && amount <= 1000000) pets = math.min(1000000, pets + amount)
              else JOptionPane.showMessageDialog(frame, "Enter a value from 0 to 1000000.")
              saveGame()
            } catch { case _: NumberFormatException => JOptionPane.showMessageDialog(frame, "Enter a valid whole number.") }
          }
        case 3 => showBiomes(true)
        case 4 => eventActive = true
        case 5 => eventActive = false
        case 6 => JOptionPane.showMessageDialog(frame, "Player: " + playerName + "\nPlayer ID: " + playerId + "\nBixuBucks: " + coins + "\nSpeed: " + speed + "\nEggs delivered: " + eggsDelivered + "\nPets: " + pets + "\nBiome: " + biomes(selectedBiome).name)
        case _ => open = false
      }
      if (gamePanel != null) gamePanel.repaint()
    }
  }

  private class GamePanel extends JPanel {
    private var px = 165.0
    private var py = 492.0
    private var p2x = 255.0
    private var p2y = 535.0
    private var carrierPlayer = 1
    private var guardianX = 865.0
    private var guardianY = 290.0
    private var carryingEgg = false
    private var nestHasEgg = true
    private var guardianChasing = false
    private var respawnTicks = 0
    private var tick = 0
    private var left = false
    private var right = false
    private var up = false
    private var down = false
    private var p2Left = false
    private var p2Right = false
    private var p2Up = false
    private var p2Down = false
    private var trainHeld = false
    private var trainCounter = 0
    private var incomeCounter = 0
    private var running = false
    private var statusMessage = "Go to the guardian nest and press E to take the egg."
    private val baseX = 170.0
    private val baseY = 470.0
    private val treadmillX = 235.0
    private val treadmillY = 405.0
    private val nestX = 905.0
    private val nestY = 275.0
    private val colors = Array(new Color(255, 244, 190), new Color(190, 231, 255), new Color(255, 204, 222), new Color(235, 235, 245))

    setPreferredSize(new Dimension(1120, 650))
    setBackground(new Color(140, 205, 235))
    setFocusable(true)
    setupKeys()

    private val timer = new Timer(16, new ActionListener {
      override def actionPerformed(e: ActionEvent): Unit = updateGame()
    })

    def startGame(): Unit = {
      running = true
      px = baseX
      py = baseY + 20.0
      p2x = baseX + 95.0
      p2y = baseY + 55.0
      timer.start()
      requestFocusInWindow()
    }

    def stopGame(): Unit = {
      running = false
      timer.stop()
    }

    def changeBiome(): Unit = {
      guardianX = nestX - 55.0
      guardianY = nestY + 28.0
      nestHasEgg = true
      carryingEgg = false
      guardianChasing = false
      statusMessage = "New biome: " + biomes(selectedBiome).name + ". Find the guardian nest!"
      repaint()
    }

    private def setupKeys(): Unit = {
      val input = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
      val actions = getActionMap
      input.put(KeyStroke.getKeyStroke("pressed W"), "upOn")
      input.put(KeyStroke.getKeyStroke("released W"), "upOff")
      input.put(KeyStroke.getKeyStroke("pressed S"), "downOn")
      input.put(KeyStroke.getKeyStroke("released S"), "downOff")
      input.put(KeyStroke.getKeyStroke("pressed A"), "leftOn")
      input.put(KeyStroke.getKeyStroke("released A"), "leftOff")
      input.put(KeyStroke.getKeyStroke("pressed D"), "rightOn")
      input.put(KeyStroke.getKeyStroke("released D"), "rightOff")
      input.put(KeyStroke.getKeyStroke("pressed UP"), "upOn")
      input.put(KeyStroke.getKeyStroke("released UP"), "upOff")
      input.put(KeyStroke.getKeyStroke("pressed DOWN"), "downOn")
      input.put(KeyStroke.getKeyStroke("released DOWN"), "downOff")
      input.put(KeyStroke.getKeyStroke("pressed LEFT"), "leftOn")
      input.put(KeyStroke.getKeyStroke("released LEFT"), "leftOff")
      input.put(KeyStroke.getKeyStroke("pressed RIGHT"), "rightOn")
      input.put(KeyStroke.getKeyStroke("released RIGHT"), "rightOff")
      input.put(KeyStroke.getKeyStroke("pressed T"), "trainOn")
      input.put(KeyStroke.getKeyStroke("released T"), "trainOff")
      input.put(KeyStroke.getKeyStroke("pressed E"), "interactP1")
      input.put(KeyStroke.getKeyStroke("pressed ENTER"), "interactP2")
      input.put(KeyStroke.getKeyStroke("pressed LEFT"), "p2LeftOn")
      input.put(KeyStroke.getKeyStroke("released LEFT"), "p2LeftOff")
      input.put(KeyStroke.getKeyStroke("pressed RIGHT"), "p2RightOn")
      input.put(KeyStroke.getKeyStroke("released RIGHT"), "p2RightOff")
      input.put(KeyStroke.getKeyStroke("pressed UP"), "p2UpOn")
      input.put(KeyStroke.getKeyStroke("released UP"), "p2UpOff")
      input.put(KeyStroke.getKeyStroke("pressed DOWN"), "p2DownOn")
      input.put(KeyStroke.getKeyStroke("released DOWN"), "p2DownOff")
      input.put(KeyStroke.getKeyStroke("pressed M"), "biomes")

      actions.put("upOn", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = up = true })
      actions.put("upOff", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = up = false })
      actions.put("downOn", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = down = true })
      actions.put("downOff", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = down = false })
      actions.put("leftOn", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = left = true })
      actions.put("leftOff", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = left = false })
      actions.put("rightOn", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = right = true })
      actions.put("rightOff", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = right = false })
      actions.put("trainOn", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = trainHeld = true })
      actions.put("trainOff", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = trainHeld = false })
      actions.put("p2LeftOn", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = if (multiplayerMode) p2Left = true })
      actions.put("p2LeftOff", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = p2Left = false })
      actions.put("p2RightOn", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = if (multiplayerMode) p2Right = true })
      actions.put("p2RightOff", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = p2Right = false })
      actions.put("p2UpOn", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = if (multiplayerMode) p2Up = true })
      actions.put("p2UpOff", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = p2Up = false })
      actions.put("p2DownOn", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = if (multiplayerMode) p2Down = true })
      actions.put("p2DownOff", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = p2Down = false })
      actions.put("interactP1", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = interact(1) })
      actions.put("interactP2", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = if (multiplayerMode) interact(2) })
      actions.put("biomes", new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = showBiomes(false) })
    }

    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 interact(player: Int): Unit = {
      val ix = if (player == 2 && multiplayerMode) p2x else px
      val iy = if (player == 2 && multiplayerMode) p2y else py
      if (carryingEgg) {
        if (player != carrierPlayer) {
          statusMessage = "The other player is carrying the egg. Help them escape!"
        } else if (distance(ix, iy, baseX, baseY) < 105.0) {
          deliverEgg()
        } else {
          statusMessage = "Run back to your BASE and press " + (if (player == 2) "ENTER" else "E") + "!"
        }
      } else if (nestHasEgg && distance(ix, iy, nestX, nestY) < 92.0) {
        carrierPlayer = player
        carryingEgg = true
        nestHasEgg = false
        guardianChasing = true
        statusMessage = (if (player == 2) player2Name else playerName) + " stole the egg! Guardian chase - RUN TO BASE!"
      } else if (distance(ix, iy, baseX, baseY) < 105.0) {
        statusMessage = "Your base is safe. Go to the guardian nest to steal an egg."
      } else {
        statusMessage = "Get close to the NEST or your BASE, then press " + (if (player == 2) "ENTER" else "E") + "."
      }
      repaint()
    }

    private def deliverEgg(): Unit = {
      carryingEgg = false
      guardianChasing = false
      guardianX = nestX - 55.0
      guardianY = nestY + 28.0
      pets += 1
      eggsDelivered += 1L
      val reward = 60L + selectedBiome.toLong * 35L
      coins = if (Long.MaxValue - coins < reward) Long.MaxValue else coins + reward
      statusMessage = "DELIVERED! Egg hatched into a pet. Your pets now earn coins over time."
      respawnTicks = 150
      saveGame()
    }

    private def updateGame(): Unit = {
      if (!running) return
      tick += 1
      val movement = 3.0 + math.min(5.5, speed.toDouble / 50000.0)
      val p1Multiplier = if (carryingEgg && carrierPlayer == 1) 0.72 else 1.0
      val p2Multiplier = if (carryingEgg && carrierPlayer == 2) 0.72 else 1.0
      if (left) px -= movement * p1Multiplier
      if (right) px += movement * p1Multiplier
      if (up) py -= movement * p1Multiplier
      if (down) py += movement * p1Multiplier
      if (multiplayerMode) {
        if (p2Left) p2x -= movement * p2Multiplier
        if (p2Right) p2x += movement * p2Multiplier
        if (p2Up) p2y -= movement * p2Multiplier
        if (p2Down) p2y += movement * p2Multiplier
      }
      px = math.max(28.0, math.min(getWidth.toDouble - 28.0, px))
      py = math.max(125.0, math.min(getHeight.toDouble - 55.0, py))
      p2x = math.max(28.0, math.min(getWidth.toDouble - 28.0, p2x))
      p2y = math.max(125.0, math.min(getHeight.toDouble - 55.0, p2y))

      val p1OnTreadmill = distance(px, py, treadmillX, treadmillY) < 92.0
      val p2OnTreadmill = multiplayerMode && distance(p2x, p2y, treadmillX, treadmillY) < 92.0
      if (trainHeld && (p1OnTreadmill || p2OnTreadmill)) {
        trainCounter += 1
        if (trainCounter >= 18) {
          trainCounter = 0
          if (speed < MAX_SPEED) speed += 1
          if (tick % 120 == 0) saveGame()
          statusMessage = "TRAINING! Speed: " + speed
        }
      } else {
        trainCounter = 0
      }

      if (guardianChasing && carryingEgg) {
        val targetX = if (carrierPlayer == 2 && multiplayerMode) p2x else px
        val targetY = if (carrierPlayer == 2 && multiplayerMode) p2y else py
        val dx = targetX - guardianX
        val dy = targetY - guardianY
        val d = math.max(1.0, math.sqrt(dx * dx + dy * dy))
        val guardStep = 1.75 + selectedBiome.toDouble * 0.035
        guardianX += (dx / d) * guardStep
        guardianY += (dy / d) * guardStep
        if (d < 38.0) {
          carryingEgg = false
          guardianChasing = false
          nestHasEgg = true
          guardianX = nestX - 55.0
          guardianY = nestY + 28.0
          statusMessage = "The guardian tagged the egg carrier! The egg returned to the nest. Try again!"
          if (carrierPlayer == 2 && multiplayerMode) {
            p2x = baseX
            p2y = baseY
          } else {
            px = baseX
            py = baseY
          }
        }
      } else if (!guardianChasing) {
        val dx = (nestX - 55.0) - guardianX
        val dy = (nestY + 28.0) - guardianY
        guardianX += dx * 0.035
        guardianY += dy * 0.035
      }

      if (respawnTicks > 0) {
        respawnTicks -= 1
        if (respawnTicks == 0) {
          nestHasEgg = true
          statusMessage = "A new egg is ready at the guardian nest."
        }
      }

      incomeCounter += 1
      if (incomeCounter >= 60) {
        incomeCounter = 0
        if (pets > 0) {
          val income = pets.toLong * (2L + selectedBiome.toLong)
          coins = if (Long.MaxValue - coins < income) Long.MaxValue else coins + income
        }
      }
      repaint()
    }

    override protected def paintComponent(graphics: Graphics): Unit = {
      super.paintComponent(graphics)
      val g = graphics.create().asInstanceOf[Graphics2D]
      try {
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
        val w = getWidth
        val h = getHeight
        val b = biomes(selectedBiome)

        // Background and distant hills.
        g.setPaint(new GradientPaint(0, 0, b.sky, 0, h, b.ground))
        g.fillRect(0, 0, w, h)
        g.setColor(new Color(255, 255, 255, 55))
        g.fillOval(-80, 80, 330, 165)
        g.fillOval(240, 42, 360, 180)
        g.fillOval(700, 60, 360, 170)

        // 2.5D road joining the home base to the guardian's nest.
        g.setColor(new Color(91, 91, 78, 65))
        g.setStroke(new BasicStroke(105.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
        g.drawLine(280, 490, 815, 330)
        g.setColor(new Color(205, 185, 135, 190))
        g.setStroke(new BasicStroke(82.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
        g.drawLine(280, 482, 815, 322)
        g.setColor(new Color(255, 255, 255, 80))
        g.setStroke(new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
        g.drawLine(300, 478, 800, 328)

        drawBase(g)
        drawNestArea(g, b)
        drawBiomeDecor(g, w, h, selectedBiome, b.accent)

        // HUD panel.
        g.setColor(new Color(8, 20, 34, 205))
        g.fillRoundRect(14, 12, 525, 112, 16, 16)
        g.setColor(new Color(255, 218, 112))
        g.setFont(new Font("Arial", Font.BOLD, 21))
        g.drawString("SPRING SEASON - CATCH THE EGG", 28, 40)
        g.setColor(Color.WHITE)
        g.setFont(new Font("Arial", Font.BOLD, 14))
        g.drawString("BIOME: " + b.name + "   GUARDIAN: " + b.guardian, 28, 65)
        g.drawString("BIXUBUCKS: " + coins + "   SPEED: " + speed + "   PETS: " + pets + "   DELIVERED: " + eggsDelivered, 28, 89)
        g.setColor(new Color(255, 235, 175))
        g.setFont(new Font("Arial", Font.BOLD, 12))
        g.drawString(if (carryingEgg) "CARRYING AN EGG - ESCAPE!" else if (nestHasEgg) "EGG READY IN THE NEST" else "NEST IS EMPTY - WAIT FOR A NEW EGG", 28, 110)

        // Characters are drawn by their depth (y coordinate).
        drawGuardian(g, guardianX, guardianY, b)
        drawPlayer(g, px, py, carryingEgg && carrierPlayer == 1, playerName)
        if (multiplayerMode) {
          drawPlayer(g, p2x, p2y, carryingEgg && carrierPlayer == 2, player2Name)
        }

        // Bottom instructions and status.
        g.setColor(new Color(7, 18, 29, 195))
        g.fillRoundRect(14, h - 70, w - 28, 56, 13, 13)
        g.setColor(Color.WHITE)
        g.setFont(new Font("Arial", Font.BOLD, 13))
        val controls = if (multiplayerMode) {
          "P1 WASD + E     P2 ARROWS + ENTER     HOLD T ON TREADMILL: TRAIN     M: BIOMES"
        } else {
          "WASD: MOVE     E: STEAL / DELIVER     HOLD T ON TREADMILL: TRAIN     M: BIOMES"
        }
        g.drawString(controls, 28, h - 47)
        g.setColor(new Color(255, 226, 132))
        g.setFont(new Font("Arial", Font.PLAIN, 12))
        g.drawString(statusMessage, 28, h - 26)
      } finally {
        g.dispose()
      }
    }

    private def drawBase(g: Graphics2D): Unit = {
      // Floating isometric base platform.
      val top = new Polygon(Array(35, 315, 370, 95), Array(420, 365, 505, 570), 4)
      val side = new Polygon(Array(95, 370, 370, 95), Array(570, 505, 535, 600), 4)
      g.setColor(new Color(79, 94, 73))
      g.fillPolygon(side)
      g.setColor(new Color(107, 166, 93))
      g.fillPolygon(top)
      g.setColor(new Color(222, 205, 145))
      g.setStroke(new BasicStroke(4.0f))
      g.drawPolygon(top)

      // Little base house.
      g.setColor(new Color(238, 222, 177))
      g.fillRoundRect(76, 385, 112, 85, 10, 10)
      g.setColor(new Color(155, 83, 55))
      val roof = new Polygon(Array(62, 132, 202), Array(390, 338, 390), 3)
      g.fillPolygon(roof)
      g.setColor(new Color(99, 61, 43))
      g.fillRoundRect(116, 423, 31, 47, 6, 6)
      g.setColor(new Color(120, 190, 218))
      g.fillRect(86, 405, 22, 21)
      g.fillRect(156, 405, 22, 21)
      g.setColor(Color.WHITE)
      g.setFont(new Font("Arial", Font.BOLD, 13))
      g.drawString("YOUR BASE", 83, 487)

      // Pet pen with small pets.
      g.setColor(new Color(91, 59, 36))
      g.setStroke(new BasicStroke(5.0f))
      g.drawRect(215, 430, 105, 76)
      g.setColor(new Color(160, 115, 70))
      g.fillRect(216, 431, 103, 5)
      g.fillRect(216, 468, 103, 5)
      var p = 0
      while (p < math.min(pets, 5)) {
        g.setColor(new Color(248, 227, 174))
        g.fillOval(226 + p * 17, 444 + (p % 2) * 8, 15, 15)
        p += 1
      }
      g.setColor(Color.WHITE)
      g.setFont(new Font("Arial", Font.BOLD, 11))
      g.drawString("PET PEN", 238, 521)

      // Treadmill - player's training zone.
      g.setColor(new Color(45, 50, 65))
      g.fillRoundRect(190, 365, 125, 47, 9, 9)
      g.setColor(new Color(190, 205, 214))
      g.fillRoundRect(199, 373, 106, 26, 5, 5)
      g.setColor(new Color(65, 75, 88))
      var stripe = 0
      while (stripe < 7) {
        g.drawLine(202 + stripe * 15, 375, 202 + stripe * 15, 397)
        stripe += 1
      }
      g.setColor(new Color(255, 221, 95))
      g.setFont(new Font("Arial", Font.BOLD, 12))
      g.drawString("TREADMILL", 212, 355)
      g.setColor(new Color(245, 245, 245))
      g.setFont(new Font("Arial", Font.PLAIN, 10))
      g.drawString("HOLD T", 230, 428)
    }

    private def drawNestArea(g: Graphics2D, b: Biome): Unit = {
      // Guardian platform in 2.5D.
      g.setColor(new Color(45, 55, 52, 75))
      g.fillOval(695, 365, 365, 105)
      g.setColor(new Color(125, 121, 93))
      g.fillOval(700, 335, 350, 112)
      g.setColor(new Color(177, 161, 111))
      g.fillOval(716, 326, 320, 94)
      g.setColor(new Color(231, 205, 145))
      g.drawOval(716, 326, 320, 94)

      // Nest bowl and sticks.
      g.setColor(new Color(112, 72, 39))
      g.fillOval(854, 275, 112, 72)
      g.setColor(new Color(165, 112, 65))
      g.setStroke(new BasicStroke(5.0f))
      g.drawArc(850, 269, 118, 83, 5, 172)
      g.drawArc(858, 277, 105, 67, 185, 160)
      if (nestHasEgg) {
        drawEggSprite(g, 902, 286, b.accent, 1.0)
      } else {
        g.setColor(new Color(45, 35, 25, 150))
        g.setFont(new Font("Arial", Font.BOLD, 12))
        g.drawString("EGG TAKEN", 875, 320)
      }
      g.setColor(Color.WHITE)
      g.setFont(new Font("Arial", Font.BOLD, 13))
      g.drawString("GUARDIAN NEST", 838, 395)
      g.setColor(new Color(255, 255, 255, 210))
      g.setFont(new Font("Arial", Font.PLAIN, 11))
      g.drawString("Go close and press E", 850, 411)
    }

    private def drawBiomeDecor(g: Graphics2D, w: Int, h: Int, index: Int, accent: Color): Unit = {
      var x = 20
      while (x < w) {
        if (index == 21) {
          // Apple orchard trees.
          g.setColor(new Color(105, 69, 39))
          g.fillRoundRect(x + 10, 170, 14, 125, 5, 5)
          g.setColor(new Color(47, 130, 62))
          g.fillOval(x - 9, 127, 54, 70)
          g.setColor(new Color(220, 43, 54))
          g.fillOval(x + 1, 151, 11, 11)
          g.fillOval(x + 24, 166, 10, 10)
          g.fillOval(x + 10, 180, 10, 10)
        } else if (index == 0 || index == 1 || index == 10 || index == 14 || index == 19) {
          g.setColor(new Color(51, 95, 51, 190))
          g.fillRect(x + 10, 165, 10, 115)
          g.fillOval(x - 8, 125, 49, 57)
        } else if (index == 4 || index == 5 || index == 9 || index == 18) {
          g.setColor(new Color(240, 249, 255, 180))
          g.fillOval(x + 5, 160, 8, 8)
          g.fillOval(x + 25, 207, 6, 6)
        } else {
          g.setColor(new Color(accent.getRed, accent.getGreen, accent.getBlue, 160))
          g.fillOval(x + 4, 180 + (x % 3) * 10, 13, 13)
          g.fillOval(x + 23, 221, 8, 8)
        }
        x += 155
      }
    }

    private def drawGuardian(g: Graphics2D, x: Double, y: Double, b: Biome): Unit = {
      val ix = x.toInt
      val iy = y.toInt
      val scale = 0.82 + math.max(0.0, math.min(1.0, (y - 150.0) / 450.0)) * 0.32
      val bodyW = (70 * scale).toInt
      val bodyH = (48 * scale).toInt
      g.setColor(new Color(30, 30, 30, 65))
      g.fillOval(ix - bodyW / 2, iy + 20, bodyW, 17)
      g.setColor(b.guardianColor)
      g.fillOval(ix - bodyW / 2, iy - bodyH / 2, bodyW, bodyH)
      g.setColor(b.guardianColor.darker())
      g.fillOval(ix - (bodyW * 0.25).toInt, iy - (bodyH * 0.72).toInt, (bodyW * 0.5).toInt, (bodyH * 0.58).toInt)
      // Ears / horns.
      g.fillOval(ix - (bodyW * 0.37).toInt, iy - (bodyH * 0.72).toInt, (bodyW * 0.2).toInt, (bodyH * 0.35).toInt)
      g.fillOval(ix + (bodyW * 0.17).toInt, iy - (bodyH * 0.72).toInt, (bodyW * 0.2).toInt, (bodyH * 0.35).toInt)
      g.setColor(Color.WHITE)
      g.fillOval(ix - 12, iy - 8, 9, 11)
      g.fillOval(ix + 4, iy - 8, 9, 11)
      g.setColor(Color.BLACK)
      g.fillOval(ix - 9, iy - 5, 4, 5)
      g.fillOval(ix + 7, iy - 5, 4, 5)
      g.setColor(new Color(55, 35, 28))
      g.drawArc(ix - 8, iy + 2, 16, 10, 190, 160)
      g.setColor(Color.WHITE)
      g.setFont(new Font("Arial", Font.BOLD, 11))
      g.drawString(b.guardian, ix - b.guardian.length * 3, iy - (bodyH / 2) - 18)
      if (guardianChasing) {
        g.setColor(new Color(255, 80, 75))
        g.setFont(new Font("Arial", Font.BOLD, 11))
        g.drawString("CHASE!", ix - 20, iy - (bodyH / 2) - 31)
      }
    }

    private def drawPlayer(g: Graphics2D, x: Double, y: Double, hasEgg: Boolean, displayName: String): Unit = {
      val ix = x.toInt
      val iy = y.toInt
      val scale = 0.78 + math.max(0.0, math.min(1.0, (y - 130.0) / 470.0)) * 0.32
      val w = (40 * scale).toInt
      val h = (60 * scale).toInt
      g.setColor(new Color(30, 30, 30, 65))
      g.fillOval(ix - w / 2 - 5, iy + 3, w + 10, 15)
      // Legs and shoes.
      g.setColor(new Color(40, 49, 100))
      g.fillRoundRect(ix - 14, iy - 18, 11, 25, 6, 6)
      g.fillRoundRect(ix + 3, iy - 18, 11, 25, 6, 6)
      g.setColor(new Color(52, 39, 30))
      g.fillRoundRect(ix - 16, iy + 2, 15, 8, 5, 5)
      g.fillRoundRect(ix + 2, iy + 2, 15, 8, 5, 5)
      // Torso, arms and head.
      g.setColor(new Color(51, 112, 231))
      g.fillRoundRect(ix - w / 2, iy - h + 20, w, 34, 11, 11)
      g.setColor(new Color(255, 202, 158))
      g.fillRoundRect(ix - w / 2 - 7, iy - h + 25, 10, 29, 7, 7)
      g.fillRoundRect(ix + w / 2 - 3, iy - h + 25, 10, 29, 7, 7)
      g.fillOval(ix - 15, iy - h - 2, 30, 30)
      g.setColor(new Color(70, 45, 27))
      g.fillArc(ix - 16, iy - h - 5, 32, 21, 0, 180)
      g.setColor(Color.BLACK)
      g.fillOval(ix - 7, iy - h + 9, 3, 4)
      g.fillOval(ix + 4, iy - h + 9, 3, 4)
      g.setColor(Color.WHITE)
      g.setFont(new Font("Arial", Font.BOLD, 11))
      g.drawString(displayName, ix - displayName.length * 3, iy - h - 12)
      if (hasEgg) drawEggSprite(g, ix, iy - h - 22, new Color(255, 245, 196), 0.82)
    }

    private def drawEggSprite(g: Graphics2D, x: Int, y: Int, color: Color, scale: Double): Unit = {
      val w = (26 * scale).toInt
      val h = (34 * scale).toInt
      g.setColor(new Color(35, 35, 35, 45))
      g.fillOval(x - w / 2 + 2, y - h / 2 + 3, w, h)
      g.setColor(color)
      g.fillOval(x - w / 2, y - h / 2, w, h)
      g.setColor(new Color(234, 124, 148))
      g.fillOval(x - 5, y - 4, 5, 5)
      g.setColor(new Color(135, 194, 245))
      g.fillOval(x + 3, y + 5, 5, 5)
    }
  }
}

// Kojo Code Exchange script entry point.
SpringSeasonCatchTheEgg.main(Array.empty[String])