Code Sketch
bixuu02
Category: Programming
import java.awt.{AlphaComposite, BasicStroke, BorderLayout, CardLayout, Color, Component, Dimension, Font, Graphics, Graphics2D, GradientPaint, GraphicsEnvironment, Polygon, RenderingHints}
import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, MouseAdapter, MouseEvent, MouseMotionAdapter, WindowAdapter, WindowEvent}
import java.io.{BufferedReader, InputStreamReader, PrintWriter}
import java.net.{Inet4Address, InetAddress, InetSocketAddress, NetworkInterface, ServerSocket, Socket}
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.{BorderFactory, Box, BoxLayout, JButton, JComboBox, JComponent, JFrame, JLabel, JOptionPane, JPanel, JPasswordField, JScrollPane, JTextArea, JTextField, SwingConstants, SwingUtilities, Timer, WindowConstants}
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.util.Try
/**
* BIXUU SPRING SEASON - ADVANCED 3D / LAN MULTIPLAYER EDITION
* Scala 2.13, JDK 17+, Swing + software perspective polygon renderer.
*
* This is a genuine perspective-projected 3D scene rendered in Java2D, not a
* photorealistic Unreal/Unity renderer. It uses 3D world coordinates, a movable
* third-person camera, depth-sorted polygon meshes, low-poly models and lighting.
*/
object BixuuSpringSeasonMultiplayer {
val APP_TITLE = "BIXUU - SPRING SEASON 3D"
val DEFAULT_PORT = 5050
val MAX_PLAYERS = 10
// Local prototype credentials. Do not expose this prototype directly to the Internet.
private val ADMIN1_NAME = "Bixuu"
private val ADMIN1_PIN = "BIXUU-2026"
private val ADMIN2_NAME = "Admin2"
private val ADMIN2_PIN = "yadbha2026"
val WORLD_W = 100.0
val WORLD_D = 84.0
val WHITE = new Color(245, 251, 255)
val MUTED = new Color(168, 193, 210)
val CYAN = new Color(74, 238, 239)
val MINT = new Color(135, 255, 190)
val PINK = new Color(255, 117, 202)
val PURPLE = new Color(176, 123, 255)
val GOLD = new Color(255, 220, 124)
val DARK = new Color(7, 15, 35)
final case class V3(x: Double, y: Double, z: Double) {
def +(b: V3): V3 = V3(x + b.x, y + b.y, z + b.z)
def -(b: V3): V3 = V3(x - b.x, y - b.y, z - b.z)
def *(s: Double): V3 = V3(x * s, y * s, z * s)
}
final case class Face(points: Vector[V3], color: Color)
final case class Wire(points: Vector[V3], color: Color, width: Float)
final case class ScreenPoint(x: Int, y: Int, depth: Double)
final case class LevelInfo(name: String, subtitle: String, hue: Int, accent: Color, crystalCount: Int, guardCount: Int)
final case class RemotePlayer(id: String, name: String, x: Double, z: Double)
final class Crystal(val x: Double, val z: Double, val hue: Int, val label: String) { @volatile var collected = false }
final class Tree(val x: Double, val z: Double, val scale: Double, val hue: Int)
final class Flower(val x: Double, val z: Double, val scale: Double, val hue: Int, val phase: Double)
final class Guard(val baseX: Double, val baseZ: Double, val radius: Double, val speed: Double, val phase: Double, val hue: Int) {
var x = baseX; var z = baseZ; var alert = false
}
final class Particle(var x: Double, var y: Double, var z: Double, var vx: Double, var vy: Double, var vz: Double, val hue: Int, var life: Double, val size: Double)
val levels = Vector(
LevelInfo("WHISPERING WOODS", "Nature Realm - restore the lost spring seeds", 145, new Color(93, 224, 149), 5, 1),
LevelInfo("AURORA FALLS", "Magic Realm - awaken the crystal waterfall", 190, new Color(91, 218, 245), 6, 2),
LevelInfo("CELESTIAL TEMPLE", "Space Realm - unlock the ancient star gate", 270, new Color(188, 133, 255), 7, 3),
LevelInfo("MONSOON GARDEN", "Weather Realm - bring rain back to the valley", 205, new Color(94, 176, 255), 7, 3),
LevelInfo("BIO-TECH SANCTUARY", "Technology Realm - restart the Bloom Engine", 165, new Color(255, 184, 98), 8, 4)
)
val random = new java.util.Random(20261031L)
val crystals = ArrayBuffer[Crystal]()
val trees = ArrayBuffer[Tree]()
val flowers = ArrayBuffer[Flower]()
val guards = ArrayBuffer[Guard]()
val particles = ArrayBuffer[Particle]()
val completedLevels = mutable.Set[Int]()
val extraEvents = ArrayBuffer[String]("GLITCH IN THE GARDEN", "AURORA BLOOM FESTIVAL")
val pressedKeys = mutable.Set[Int]()
@volatile var currentUser = "Player"
@volatile var currentRole = "PLAYER"
@volatile var isAdmin = false
@volatile var isSuperAdmin = false
@volatile var activeLevel = 0
@volatile var multiplayerMode = "SOLO"
@volatile var activeEventTitle = "AURORA FALLS: THE LOST SPRING"
@volatile var announcement = "Welcome to Bixuu Spring Season!"
@volatile var weatherMode = "SPRING BREEZE"
var playerX = 8.0
var playerZ = 72.0
var cameraYaw = 0.0
var cameraPitch = 0.24
var cameraX = 0.0
var cameraY = 0.0
var cameraZ = 0.0
var frameCount = 0L
var remainingSeconds = 420.0
var health = 3
var coins = 0
var started = false
var paused = false
var won = false
var lost = false
var showGlow = true
var showMiniMap = true
var dashTime = 0.0
var dashCooldown = 0.0
var hitCooldown = 0.0
var toast = "Explore the Spring Valley and collect every crystal."
var toastTime = 4.0
var mouseLastX = -1
var mouseLastY = -1
val portalX = 89.0
val portalZ = 11.0
val spawnX = 8.0
val spawnZ = 72.0
var frame: JFrame = _
var root: JPanel = _
var cardLayout: CardLayout = _
var gamePanel: GamePanel = _
var gameTimer: Timer = _
var playerNameField: JTextField = _
var adminNameChoice: JComboBox[String] = _
var adminPasswordField: JPasswordField = _
var adminButton: JButton = _
var welcomeLabel: JLabel = _
var levelButtons = ArrayBuffer[JButton]()
var hostIpLabel: JLabel = _
var networkStatus: JLabel = _
var eventTitleField: JTextField = _
var eventAddField: JTextField = _
var announcementField: JTextField = _
var eventListArea: JTextArea = _
var weatherChoice: JComboBox[String] = _
var lanServer: Option[LanServer] = None
var lanClient: Option[LanClient] = None
def clamp(v: Double, lo: Double, hi: Double): Double = math.max(lo, math.min(hi, v))
def distance(x1: Double, z1: Double, x2: Double, z2: Double): Double = math.hypot(x1 - x2, z1 - z2)
def safeName(s: String): String = Option(s).getOrElse("").trim.replaceAll("[,;|\\r\\n]", "_").take(18) match { case "" => "Player"; case v => v }
def colorHue(h: Int, sat: Float = 0.72f, bri: Float = 1.0f): Color = Color.getHSBColor((h / 360.0).toFloat, sat, bri)
def showToast(s: String, seconds: Double = 2.6): Unit = { toast = s; toastTime = seconds }
def collectedCount: Int = crystals.count(_.collected)
// Colorful gradient buttons with a visible hover / press response.
final class NeonButton(text: String, base: Color = new Color(16, 153, 142), edge: Color = CYAN) extends JButton(text) {
private var hovering = false
setFont(new Font("SansSerif", Font.BOLD, 14))
setForeground(WHITE); setFocusPainted(false); setContentAreaFilled(false); setBorderPainted(false); setOpaque(false)
setPreferredSize(new Dimension(300, 44)); setMaximumSize(new Dimension(390, 48)); setAlignmentX(Component.CENTER_ALIGNMENT)
setBorder(BorderFactory.createEmptyBorder(10, 18, 10, 18))
addMouseListener(new MouseAdapter {
override def mouseEntered(e: MouseEvent): Unit = { hovering = true; repaint() }
override def mouseExited(e: MouseEvent): Unit = { hovering = false; repaint() }
})
override protected def paintComponent(g0: Graphics): Unit = {
val g = g0.create().asInstanceOf[Graphics2D]
try {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
val c1 = if (hovering) edge.brighter() else base.brighter()
val c2 = if (hovering) base.brighter() else base.darker()
g.setPaint(new GradientPaint(0, 0, c1, 0, getHeight, c2))
g.fillRoundRect(1, 1, getWidth - 2, getHeight - 2, 18, 18)
g.setColor(new Color(edge.getRed, edge.getGreen, edge.getBlue, if (hovering) 235 else 170))
g.setStroke(new BasicStroke(if (hovering) 2.3f else 1.2f))
g.drawRoundRect(1, 1, getWidth - 3, getHeight - 3, 18, 18)
if (getModel.isPressed) { g.setColor(new Color(0, 0, 0, 45)); g.fillRoundRect(1, 1, getWidth - 2, getHeight - 2, 18, 18) }
} finally g.dispose()
super.paintComponent(g0)
}
}
def styledButton(text: String, color: Color = new Color(16, 153, 142), edge: Color = CYAN): JButton = new NeonButton(text, color, edge)
def heading(text: String, size: Int, color: Color): JLabel = { val l = new JLabel(text, SwingConstants.CENTER); l.setFont(new Font("SansSerif", Font.BOLD, size)); l.setForeground(color); l.setAlignmentX(Component.CENTER_ALIGNMENT); l }
def bodyLabel(text: String): JLabel = { val l = new JLabel(text, SwingConstants.CENTER); l.setFont(new Font("SansSerif", Font.PLAIN, 13)); l.setForeground(MUTED); l.setAlignmentX(Component.CENTER_ALIGNMENT); l }
def fieldStyle(f: JTextField): Unit = { f.setFont(new Font("SansSerif", Font.PLAIN, 14)); f.setBackground(new Color(14, 30, 57)); f.setForeground(WHITE); f.setCaretColor(CYAN); f.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(68, 185, 180)), BorderFactory.createEmptyBorder(7, 9, 7, 9))) }
def verticalPanel(): JPanel = { val p = new JPanel(); p.setOpaque(true); p.setBackground(new Color(7, 17, 39)); p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); p.setBorder(BorderFactory.createEmptyBorder(22, 32, 22, 32)); p }
def addCentered(p: JPanel, c: JComponent, gap: Int = 9): Unit = { p.add(Box.createVerticalStrut(gap)); c.setAlignmentX(Component.CENTER_ALIGNMENT); p.add(c) }
def showCard(name: String): Unit = { cardLayout.show(root, name); root.revalidate(); root.repaint() }
def addAction(b: JButton)(action: => Unit): Unit = b.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = action })
def localIPv4: String = {
val found = ArrayBuffer[String]()
try {
val en = NetworkInterface.getNetworkInterfaces
while (en.hasMoreElements) { val ni = en.nextElement(); if (ni.isUp && !ni.isLoopback) { val ae = ni.getInetAddresses; while (ae.hasMoreElements) { val a = ae.nextElement(); if (a.isInstanceOf[Inet4Address] && !a.isLoopbackAddress) found += a.getHostAddress } } }
} catch { case _: Throwable => () }
found.headOption.getOrElse("127.0.0.1")
}
// ------------------------- LAN server -------------------------
final class LanServer(port: Int, hostName: String, level: Int) {
private val socket = new ServerSocket()
private val running = new AtomicBoolean(true)
private val nextId = new java.util.concurrent.atomic.AtomicInteger(1)
private val sessions = new ConcurrentHashMap[String, ServerPeer]()
private val sharedGems = ConcurrentHashMap.newKeySet[String]()
@volatile private var hostX = spawnX
@volatile private var hostZ = spawnZ
private class ServerPeer(val id: String, val name: String, val sock: Socket, val out: PrintWriter) { @volatile var x = spawnX; @volatile var z = spawnZ }
socket.setReuseAddress(true); socket.bind(new InetSocketAddress(port))
def start(): Unit = {
val acceptThread = new Thread(new Runnable { override def run(): Unit = {
while (running.get()) try { val s = socket.accept(); s.setTcpNoDelay(true); val t = new Thread(new Runnable { override def run(): Unit = handle(s) }); t.setDaemon(true); t.start() } catch { case _: java.net.SocketException => (); case _: Throwable => () }
}}, "Bixuu-LAN-Accept")
acceptThread.setDaemon(true); acceptThread.start()
val broadcastThread = new Thread(new Runnable { override def run(): Unit = {
while (running.get()) { broadcastState(); try Thread.sleep(90L) catch { case _: InterruptedException => Thread.currentThread.interrupt() } }
}}, "Bixuu-LAN-Broadcast")
broadcastThread.setDaemon(true); broadcastThread.start()
}
private def handle(s: Socket): Unit = {
var peer: ServerPeer = null
try {
val in = new BufferedReader(new InputStreamReader(s.getInputStream, "UTF-8"))
val out = new PrintWriter(s.getOutputStream, true)
val first = in.readLine()
if (first == null) { s.close(); return }
val p = first.split("\\|", -1)
if (p.length < 3 || p(0) != "JOIN") { out.println("ERROR|Bad join packet"); s.close(); return }
val name = safeName(p(1)); val requestedLevel = Try(p(2).toInt).getOrElse(-1)
if (requestedLevel != level) { out.println("ERROR|Room level does not match"); s.close(); return }
if (sessions.size() >= MAX_PLAYERS - 1) { out.println("ERROR|Room is full (10 players maximum)"); s.close(); return }
val id = name + "-" + nextId.getAndIncrement()
peer = new ServerPeer(id, name, s, out); sessions.put(id, peer)
out.println("WELCOME|" + id)
var line = in.readLine()
while (line != null && running.get()) {
if (line.startsWith("POS|")) {
val xy = line.substring(4).split(",", 2)
if (xy.length == 2) { val x = Try(xy(0).toDouble).getOrElse(peer.x); val z = Try(xy(1).toDouble).getOrElse(peer.z); peer.x = clamp(x, 0.0, WORLD_W); peer.z = clamp(z, 0.0, WORLD_D) }
} else if (line.startsWith("GEM|")) {
val i = Try(line.substring(4).toInt).getOrElse(-1)
if (i >= 0 && i < 12) sharedGems.add(i.toString)
}
line = in.readLine()
}
} catch { case _: Throwable => () }
finally { if (peer != null) sessions.remove(peer.id); try s.close() catch { case _: Throwable => () } }
}
def publishHostPosition(x: Double, z: Double): Unit = { hostX = clamp(x, 0, WORLD_W); hostZ = clamp(z, 0, WORLD_D) }
def markGem(i: Int): Unit = if (i >= 0 && i < 12) sharedGems.add(i.toString)
def collectedGems: Set[Int] = { val b = mutable.Set[Int](); val it = sharedGems.iterator(); while (it.hasNext) Try(it.next().toInt).foreach(i => { b += i; () }); b.toSet }
def remotePlayers: Vector[RemotePlayer] = { val b = Vector.newBuilder[RemotePlayer]; val it = sessions.values().iterator(); while (it.hasNext) { val p = it.next(); b += RemotePlayer(p.id, p.name, p.x, p.z) }; b.result() }
private def stateLine: String = {
val all = ArrayBuffer[String]("HOST," + safeName(hostName) + "," + hostX + "," + hostZ)
val it = sessions.values().iterator(); while (it.hasNext) { val p = it.next(); all += (p.id + "," + p.name + "," + p.x + "," + p.z) }
"STATE|" + all.mkString(";") + "|" + sharedGems.toArray.mkString(",")
}
private def broadcastState(): Unit = { val msg = stateLine; val it = sessions.values().iterator(); while (it.hasNext) { val p = it.next(); try p.out.synchronized { p.out.println(msg) } catch { case _: Throwable => () } } }
def close(): Unit = { running.set(false); try socket.close() catch { case _: Throwable => () }; val it = sessions.values().iterator(); while (it.hasNext) try it.next().sock.close() catch { case _: Throwable => () }; sessions.clear(); sharedGems.clear() }
}
// ------------------------- LAN client -------------------------
final class LanClient(host: String, port: Int, playerName: String, level: Int) {
private val socket = new Socket(); socket.connect(new InetSocketAddress(host, port), 4500); socket.setTcpNoDelay(true)
private val out = new PrintWriter(socket.getOutputStream, true)
private val in = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
private val players = new ConcurrentHashMap[String, RemotePlayer]()
private val gems = ConcurrentHashMap.newKeySet[String]()
@volatile private var connected = true
out.println("JOIN|" + safeName(playerName) + "|" + level)
private val welcome = in.readLine()
if (welcome == null || welcome.startsWith("ERROR|")) { socket.close(); throw new java.io.IOException(if (welcome == null) "Server closed the connection" else welcome.substring(6)) }
private val id = if (welcome.startsWith("WELCOME|")) welcome.substring(8) else safeName(playerName)
private val reader = new Thread(new Runnable { override def run(): Unit = {
try { var line = in.readLine(); while (line != null && connected) {
if (line.startsWith("STATE|")) {
val sections = line.split("\\|", 3); players.clear()
if (sections.length >= 2 && sections(1).nonEmpty) sections(1).split(";").foreach { e => val q = e.split(",", 4); if (q.length == 4) { val rp = RemotePlayer(q(0), q(1), Try(q(2).toDouble).getOrElse(0.0), Try(q(3).toDouble).getOrElse(0.0)); if (rp.id != id) players.put(rp.id, rp) } }
gems.clear(); if (sections.length == 3 && sections(2).nonEmpty) sections(2).split(",").foreach(gems.add)
}
line = in.readLine()
}} catch { case _: Throwable => () }; connected = false
}}, "Bixuu-LAN-Reader")
reader.setDaemon(true); reader.start()
def isConnected: Boolean = connected
def sendPosition(x: Double, z: Double): Unit = if (connected) out.synchronized { out.println("POS|" + x + "," + z) }
def collectGem(i: Int): Unit = if (connected && i >= 0 && i < 12) out.synchronized { out.println("GEM|" + i) }
def collectedGems: Set[Int] = { val b = mutable.Set[Int](); val it = gems.iterator(); while (it.hasNext) Try(it.next().toInt).foreach(i => { b += i; () }); b.toSet }
def remotePlayers: Vector[RemotePlayer] = { val b = Vector.newBuilder[RemotePlayer]; val it = players.values().iterator(); while (it.hasNext) b += it.next(); b.result() }
def close(): Unit = { connected = false; try socket.close() catch { case _: Throwable => () } }
}
// ------------------------- World setup -------------------------
def terrainHeight(x: Double, z: Double): Double = {
val rolling = math.sin(x * 0.16) * 0.38 + math.cos(z * 0.13) * 0.28 + math.sin((x + z) * 0.09) * 0.22
rolling + 0.12 * math.sin(x * 0.48) * math.cos(z * 0.36)
}
def setupLevel(level: Int): Unit = {
activeLevel = level.max(0).min(levels.length - 1)
crystals.clear(); trees.clear(); flowers.clear(); guards.clear(); particles.clear(); pressedKeys.clear()
val gemNames = Vector("MINT SEED", "SUN PETAL", "STAR BLOOM", "MOON GEM", "SPRING ORB", "AURORA HEART", "RAIN CRYSTAL", "TECH SEED")
val count = levels(activeLevel).crystalCount
var i = 0
while (i < count) { val x = 17.0 + ((i * 13.7 + 9.0) % 72.0); val z = 63.0 - ((i * 9.3 + 4.0) % 50.0); crystals += new Crystal(x, z, Vector(145, 48, 190, 285, 315, 205, 170, 28)(i % 8), gemNames(i % gemNames.length)); i += 1 }
i = 0
while (i < 44) { val x = 3.0 + random.nextDouble() * 94.0; val z = 4.0 + random.nextDouble() * 76.0; if (distance(x, z, spawnX, spawnZ) > 8 && distance(x, z, portalX, portalZ) > 8) trees += new Tree(x, z, 0.72 + random.nextDouble() * 0.72, (levels(activeLevel).hue + random.nextInt(34) - 17 + 360) % 360); i += 1 }
i = 0
while (i < 320) { flowers += new Flower(random.nextDouble() * WORLD_W, random.nextDouble() * WORLD_D, 0.55 + random.nextDouble() * 0.75, Vector(145, 165, 188, 285, 315, 48)(random.nextInt(6)), random.nextDouble() * 6.28); i += 1 }
i = 0
while (i < levels(activeLevel).guardCount) { val x = 28.0 + i * 17.0; val z = 37.0 + (i % 2) * 16.0; guards += new Guard(x, z, 7.0 + i, 0.8 + i * 0.12, i * 1.7, if (i % 2 == 0) 330 else 275); i += 1 }
playerX = spawnX; playerZ = spawnZ; cameraYaw = 0.0; cameraPitch = 0.24
frameCount = 0L; remainingSeconds = 420.0; health = 3; coins = 0
started = true; paused = false; won = false; lost = false; dashTime = 0; dashCooldown = 0; hitCooldown = 0; showGlow = true; showMiniMap = true
showToast("" + levels(activeLevel).name + " - collect every crystal and reach the portal.", 4.0)
}
def startGame(level: Int, mode: String): Unit = {
multiplayerMode = mode; setupLevel(level)
if (gameTimer != null) gameTimer.stop()
if (gamePanel != null) root.remove(gamePanel)
gamePanel = new GamePanel(); root.add(gamePanel, "game"); showCard("game")
gameTimer = new Timer(16, new ActionListener { override def actionPerformed(e: ActionEvent): Unit = { updateGame(0.016); if (gamePanel != null) gamePanel.repaint() } })
gameTimer.start(); gamePanel.requestFocusInWindow()
}
def closeNetwork(): Unit = { lanClient.foreach(_.close()); lanClient = None; lanServer.foreach(_.close()); lanServer = None }
def returnToLevels(): Unit = { if (gameTimer != null) gameTimer.stop(); closeNetwork(); showCard("levels"); refreshLevelButtons() }
def restartCurrentLevel(): Unit = { setupLevel(activeLevel); if (gamePanel != null) gamePanel.requestFocusInWindow() }
def spawnBurst(x: Double, y: Double, z: Double, hue: Int, amount: Int): Unit = { var i = 0; while (i < amount) { val a = random.nextDouble() * math.Pi * 2; val s = 1.5 + random.nextDouble() * 4.5; particles += new Particle(x, y, z, math.cos(a) * s, random.nextDouble() * 3.5, math.sin(a) * s, hue, 0.6 + random.nextDouble() * 1.1, 0.06 + random.nextDouble() * 0.09); i += 1 } }
def collectOrUse(): Unit = {
if (!started || paused || won || lost) return
val nearest = crystals.filter(!_.collected).sortBy(c => distance(playerX, playerZ, c.x, c.z)).headOption
nearest match {
case Some(c) if distance(playerX, playerZ, c.x, c.z) < 2.5 =>
val idx = crystals.indexOf(c); c.collected = true; coins += 25
if (multiplayerMode == "HOST") lanServer.foreach(_.markGem(idx))
if (multiplayerMode == "JOIN") lanClient.foreach(_.collectGem(idx))
showToast("CRYSTAL FOUND: " + c.label + " [" + collectedCount + "/" + crystals.length + "]", 2.5); spawnBurst(c.x, 1.4, c.z, c.hue, 28)
if (collectedCount == crystals.length) showToast("ALL CRYSTALS FOUND! Reach the portal.", 4.0)
case _ => if (distance(playerX, playerZ, portalX, portalZ) < 3.6) {
if (collectedCount == crystals.length) { won = true; completedLevels += activeLevel; coins += 150; showToast("REALM COMPLETE! +150 BLOOM COINS", 8.0); spawnBurst(portalX, 2.0, portalZ, 160, 90) }
else showToast("PORTAL LOCKED - collect all crystals first.", 2.8)
} else showToast("Move closer to a crystal or portal, then press E.", 2.4)
}
}
def updateGame(dt: Double): Unit = {
frameCount += 1
if (toastTime > 0) toastTime = math.max(0, toastTime - dt)
if (dashTime > 0) dashTime = math.max(0, dashTime - dt)
if (dashCooldown > 0) dashCooldown = math.max(0, dashCooldown - dt)
if (hitCooldown > 0) hitCooldown = math.max(0, hitCooldown - dt)
particles.foreach { p => p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt; p.vy -= 2.4 * dt; p.life -= dt }
particles.filter(_.life <= 0).toVector.foreach(particles -= _)
if (!started || paused || won || lost) return
remainingSeconds = math.max(0, remainingSeconds - dt)
if (remainingSeconds <= 0) { lost = true; showToast("TIME OVER! Press R to retry.", 8.0); return }
val forwardX = math.sin(cameraYaw); val forwardZ = math.cos(cameraYaw)
val rightX = math.cos(cameraYaw); val rightZ = -math.sin(cameraYaw)
var ix = 0.0; var iz = 0.0
if (pressedKeys.contains(KeyEvent.VK_W) || pressedKeys.contains(KeyEvent.VK_UP)) { ix += forwardX; iz += forwardZ }
if (pressedKeys.contains(KeyEvent.VK_S) || pressedKeys.contains(KeyEvent.VK_DOWN)) { ix -= forwardX; iz -= forwardZ }
if (pressedKeys.contains(KeyEvent.VK_A) || pressedKeys.contains(KeyEvent.VK_LEFT)) { ix -= rightX; iz -= rightZ }
if (pressedKeys.contains(KeyEvent.VK_D) || pressedKeys.contains(KeyEvent.VK_RIGHT)) { ix += rightX; iz += rightZ }
val len = math.hypot(ix, iz)
if (len > 0.001) { ix /= len; iz /= len; val speed = if (dashTime > 0) 11.5 else 5.1; playerX = clamp(playerX + ix * speed * dt, 1.5, WORLD_W - 1.5); playerZ = clamp(playerZ + iz * speed * dt, 1.5, WORLD_D - 1.5) }
guards.foreach { g =>
val d = distance(playerX, playerZ, g.x, g.z)
g.alert = d < 15.0
if (g.alert) { val dx = playerX - g.x; val dz = playerZ - g.z; val n = math.max(0.001, math.hypot(dx, dz)); g.x += dx / n * g.speed * 2.8 * dt; g.z += dz / n * g.speed * 2.8 * dt }
else { g.x = g.baseX + math.sin(frameCount * 0.018 * g.speed + g.phase) * g.radius; g.z = g.baseZ + math.cos(frameCount * 0.014 + g.phase) * 2.0 }
if (hitCooldown <= 0 && d < 1.35 && dashTime <= 0) { health -= 1; hitCooldown = 1.8; playerX = clamp(playerX - (g.x - playerX) * 1.2, 1.5, WORLD_W - 1.5); playerZ = clamp(playerZ - (g.z - playerZ) * 1.2, 1.5, WORLD_D - 1.5); showToast("A SHADOW GUARD CAUGHT YOU! Hearts: " + health, 2.0); spawnBurst(playerX, 1.2, playerZ, 330, 18); if (health <= 0) { lost = true; showToast("ADVENTURE OVER! Press R to retry.", 8.0) } }
}
multiplayerMode match {
case "HOST" => lanServer.foreach { s => s.collectedGems.foreach(i => if (i >= 0 && i < crystals.length) crystals(i).collected = true); s.publishHostPosition(playerX, playerZ) }
case "JOIN" => lanClient.foreach { c => c.collectedGems.foreach(i => if (i >= 0 && i < crystals.length) crystals(i).collected = true); c.sendPosition(playerX, playerZ) }
case _ => ()
}
}
// ------------------------- 3D math / renderer -------------------------
def addCube(out: ArrayBuffer[Face], cx: Double, cy: Double, cz: Double, sx: Double, sy: Double, sz: Double, c: Color): Unit = {
val x0 = cx - sx / 2; val x1 = cx + sx / 2; val y0 = cy - sy / 2; val y1 = cy + sy / 2; val z0 = cz - sz / 2; val z1 = cz + sz / 2
val p000 = V3(x0,y0,z0); val p001 = V3(x0,y0,z1); val p010 = V3(x0,y1,z0); val p011 = V3(x0,y1,z1)
val p100 = V3(x1,y0,z0); val p101 = V3(x1,y0,z1); val p110 = V3(x1,y1,z0); val p111 = V3(x1,y1,z1)
out += Face(Vector(p000,p001,p011,p010), c.darker()); out += Face(Vector(p100,p110,p111,p101), c.brighter())
out += Face(Vector(p000,p100,p101,p001), c.darker().darker()); out += Face(Vector(p010,p011,p111,p110), c.brighter())
out += Face(Vector(p000,p010,p110,p100), c.darker()); out += Face(Vector(p001,p101,p111,p011), c)
}
def addSphere(out: ArrayBuffer[Face], center: V3, rx: Double, ry: Double, rz: Double, base: Color, rings: Int = 5, segments: Int = 8): Unit = {
def point(r: Int, s: Int): V3 = { val lat = -math.Pi / 2 + math.Pi * r / rings; val lon = math.Pi * 2 * s / segments; V3(center.x + math.cos(lat) * math.cos(lon) * rx, center.y + math.sin(lat) * ry, center.z + math.cos(lat) * math.sin(lon) * rz) }
var r = 0; while (r < rings) { var s = 0; while (s < segments) { val a = point(r,s); val b = point(r,s+1); val c = point(r+1,s+1); val d = point(r+1,s); val factor = (0.72 + 0.28 * (r.toDouble / rings)); val col = new Color((base.getRed*factor).toInt.min(255), (base.getGreen*factor).toInt.min(255), (base.getBlue*factor).toInt.min(255)); out += Face(Vector(a,b,c,d), col); s += 1 }; r += 1 }
}
def addCrystalMesh(out: ArrayBuffer[Face], x: Double, z: Double, hue: Int): Unit = {
val y = terrainHeight(x,z) + 1.35; val c = colorHue(hue); val top = V3(x,y+0.85,z); val bottom = V3(x,y-0.72,z)
val ring = Vector(V3(x-0.48,y,z),V3(x,y,z+0.48),V3(x+0.48,y,z),V3(x,y,z-0.48))
var i=0; while(i<4){ val a=ring(i); val b=ring((i+1)%4); out += Face(Vector(top,a,b),c.brighter()); out += Face(Vector(bottom,b,a),c.darker()); i+=1 }
}
def addTreeMesh(out: ArrayBuffer[Face], t: Tree): Unit = {
val gy = terrainHeight(t.x,t.z); addCube(out,t.x,gy+1.55*t.scale,t.z,0.38*t.scale,3.1*t.scale,0.38*t.scale,new Color(104,67,43))
addSphere(out,V3(t.x,gy+3.5*t.scale,t.z),1.45*t.scale,1.55*t.scale,1.35*t.scale,colorHue(t.hue,0.65f,0.88f),5,7)
addSphere(out,V3(t.x-0.72*t.scale,gy+3.0*t.scale,t.z+0.15),0.85*t.scale,0.95*t.scale,0.84*t.scale,colorHue((t.hue+12)%360,0.62f,0.9f),4,6)
}
def addHumanMesh(out: ArrayBuffer[Face], x: Double, z: Double, cloth: Color, local: Boolean, bob: Double = 0): Unit = {
val y = terrainHeight(x,z)
addCube(out,x,y+0.95+bob,z,0.72,1.12,0.48,cloth)
addSphere(out,V3(x,y+1.83+bob,z),0.37,0.39,0.36,new Color(237,190,151),5,8)
addCube(out,x-0.22,y+0.36,z,0.20,0.72,0.22,new Color(35,48,76)); addCube(out,x+0.22,y+0.36,z,0.20,0.72,0.22,new Color(35,48,76))
addCube(out,x-0.22,y+0.02,z+0.08,0.34,0.16,0.48,new Color(47,220,210)); addCube(out,x+0.22,y+0.02,z+0.08,0.34,0.16,0.48,new Color(47,220,210))
addCube(out,x-0.48,y+1.05+bob,z,0.20,0.78,0.22,cloth.darker()); addCube(out,x+0.48,y+1.05+bob,z,0.20,0.78,0.22,cloth.darker())
if (local) addCube(out,x,y+2.18+bob,z,0.70,0.14,0.60,new Color(20,55,88))
}
def addPortalMesh(out: ArrayBuffer[Face], wires: ArrayBuffer[Wire]): Unit = {
val baseY = terrainHeight(portalX,portalZ); val center = V3(portalX,baseY+2.6,portalZ); val major=2.05; val tube=0.18; val segments=28; val sides=6
var i=0; while(i<segments){ val a0=i*math.Pi*2/segments; val a1=(i+1)*math.Pi*2/segments; val p0=V3(center.x+math.cos(a0)*major,center.y,center.z); val p1=V3(center.x+math.cos(a1)*major,center.y,center.z); wires += Wire(Vector(V3(p0.x,center.y-major,p0.z),V3(p1.x,center.y-major,p1.z)),if(i%2==0) CYAN else PURPLE,4.0f); var j=0; while(j<sides){ val u0=j*math.Pi*2/sides; val u1=(j+1)*math.Pi*2/sides; def q(a:Double,u:Double)=V3(center.x+(major+tube*math.cos(u))*math.cos(a),center.y+tube*math.sin(u),center.z+(major+tube*math.cos(u))*math.sin(a)); out += Face(Vector(q(a0,u0),q(a1,u0),q(a1,u1),q(a0,u1)),if(i%2==0) new Color(76,224,238) else new Color(151,104,255)); j+=1 }; i+=1 }
addCube(out,portalX,baseY+0.05,portalZ,5.4,0.18,3.0,new Color(61,91,125))
}
def cameraBasis(): (V3,V3,V3,V3) = {
val forwardPlanar=V3(math.sin(cameraYaw),0,math.cos(cameraYaw)); val cam=V3(playerX-forwardPlanar.x*11.5,terrainHeight(playerX,playerZ)+8.2,playerZ-forwardPlanar.z*11.5)
val target=V3(playerX,terrainHeight(playerX,playerZ)+1.3,playerZ)
val f0=target-cam; val fl=math.sqrt(f0.x*f0.x+f0.y*f0.y+f0.z*f0.z); val f=V3(f0.x/fl,f0.y/fl,f0.z/fl)
val up0=V3(0,1,0); val r0=V3(up0.y*f.z-up0.z*f.y,up0.z*f.x-up0.x*f.z,up0.x*f.y-up0.y*f.x); val rl=math.sqrt(r0.x*r0.x+r0.y*r0.y+r0.z*r0.z); val r=V3(r0.x/rl,r0.y/rl,r0.z/rl)
val u=V3(f.y*r.z-f.z*r.y,f.z*r.x-f.x*r.z,f.x*r.y-f.y*r.x)
cameraX=cam.x; cameraY=cam.y; cameraZ=cam.z; (cam,f,r,u)
}
def project(v: V3, cam: V3, f: V3, r: V3, u: V3, w: Int, h: Int): Option[ScreenPoint] = {
val d=v-cam; val z=d.x*f.x+d.y*f.y+d.z*f.z
if (z <= 0.45) None else { val cx=d.x*r.x+d.y*r.y+d.z*r.z; val cy=d.x*u.x+d.y*u.y+d.z*u.z; val focal=math.min(w,h)*0.88; Some(ScreenPoint((w*0.5+cx*focal/z).toInt,(h*0.52-cy*focal/z).toInt,z)) }
}
def createSceneFaces(): (ArrayBuffer[Face],ArrayBuffer[Wire]) = {
val faces=ArrayBuffer[Face](); val wires=ArrayBuffer[Wire]()
// Curved ground is built from real 3D triangles, not a flat screen-space fill.
val minX=math.max(0,(playerX-35).toInt/5*5).toDouble; val maxX=math.min(WORLD_W,(playerX+35).toInt/5*5).toDouble
val minZ=math.max(0,(playerZ-42).toInt/5*5).toDouble; val maxZ=math.min(WORLD_D,(playerZ+42).toInt/5*5).toDouble
var x=minX; while(x<maxX){ var z=minZ; while(z<maxZ){ val x1=x+5; val z1=z+5; val h00=terrainHeight(x,z); val h10=terrainHeight(x1,z); val h11=terrainHeight(x1,z1); val h01=terrainHeight(x,z1); val band=if(((x/5).toInt+(z/5).toInt)%2==0) 1.0 else 0.92; val base=levels(activeLevel).accent; val c=new Color((base.getRed*band*0.58).toInt.min(255),(base.getGreen*band*0.72).toInt.min(255),(base.getBlue*band*0.60).toInt.min(255)); faces += Face(Vector(V3(x,h00,z),V3(x1,h10,z),V3(x1,h11,z1)),c); faces += Face(Vector(V3(x,h00,z),V3(x1,h11,z1),V3(x,h01,z1)),c.brighter()); z+=5 }; x+=5 }
trees.foreach(t=>if(distance(playerX,playerZ,t.x,t.z)<47) addTreeMesh(faces,t))
crystals.filter(!_.collected).foreach(c=>if(distance(playerX,playerZ,c.x,c.z)<48) addCrystalMesh(faces,c.x,c.z,c.hue))
guards.foreach { g => if(distance(playerX,playerZ,g.x,g.z)<38) { val y=terrainHeight(g.x,g.z); addSphere(faces,V3(g.x,y+1.1,g.z),0.62,0.75,0.58,colorHue(g.hue,0.68f,0.88f),5,7); addSphere(faces,V3(g.x,y+1.85,g.z),0.42,0.40,0.42,new Color(46,32,68),4,7); addSphere(faces,V3(g.x-0.15,y+1.9,g.z+0.34),0.07,0.07,0.05,PINK,3,5); addSphere(faces,V3(g.x+0.15,y+1.9,g.z+0.34),0.07,0.07,0.05,PINK,3,5) } }
addPortalMesh(faces,wires)
addHumanMesh(faces,playerX,playerZ,new Color(27,139,160),true,math.sin(frameCount*0.16)*0.04)
val others = if(multiplayerMode=="HOST") lanServer.toVector.flatMap(_.remotePlayers) else if(multiplayerMode=="JOIN") lanClient.toVector.flatMap(_.remotePlayers) else Vector.empty
others.foreach(p=>if(distance(playerX,playerZ,p.x,p.z)<45) addHumanMesh(faces,p.x,p.z,new Color(211,119,69),false))
// Flower stems / petals as low-poly colored objects.
flowers.foreach { fl => if(distance(playerX,playerZ,fl.x,fl.z)<25){ val y=terrainHeight(fl.x,fl.z); addCube(faces,fl.x,y+0.24*fl.scale,fl.z,0.045,0.48*fl.scale,0.045,new Color(47,126,65)); addSphere(faces,V3(fl.x,y+0.52*fl.scale,fl.z),0.20*fl.scale,0.12*fl.scale,0.20*fl.scale,colorHue(fl.hue),3,6) } }
// Distant waterfall cliff in the eastern valley.
addCube(faces,78,5.0,27,12,10,2,new Color(92,112,126)); addCube(faces,78,3.6,25.8,2.0,7.8,0.18,new Color(94,205,245)); addCube(faces,78,0.25,25.8,11,0.22,4,new Color(54,156,193))
(faces,wires)
}
def draw3DWorld(g: Graphics2D,w:Int,h:Int): Unit = {
val top=colorHue((levels(activeLevel).hue+210)%360,0.64f,0.21f); val bottom=new Color(10,48,65)
g.setPaint(new GradientPaint(0,0,top,0,h,bottom)); g.fillRect(0,0,w,h)
// Distant sky stars / drifting spring motes.
var i=0; while(i<85){ val px=(i*97+(frameCount/3).toInt)%(w.max(1)); val py=(i*43)%math.max(1,(h*0.43).toInt); g.setColor(new Color(235,250,255,90+(i%5)*25)); g.fillOval(px,py,2+(i%3),2+(i%3)); i+=1 }
val (cam,f,r,u)=cameraBasis(); val (faces,wires)=createSceneFaces()
val projected=faces.flatMap { face => val ps=face.points.map(project(_,cam,f,r,u,w,h)); if(ps.forall(_.nonEmpty)){ val q=ps.flatten; Some((q.map(_.depth).sum/q.length,q,face.color)) } else None }.sortBy(item => -item._1)
projected.foreach { item => val q=item._2; val poly=new Polygon(q.map(_.x).toArray,q.map(_.y).toArray,q.length); g.setColor(item._3); g.fillPolygon(poly); g.setColor(new Color(255,255,255,30)); g.setStroke(new BasicStroke(0.5f)); g.drawPolygon(poly) }
wires.foreach { line => val p=line.points.flatMap(project(_,cam,f,r,u,w,h)); if(p.length>=2){ g.setColor(line.color); g.setStroke(new BasicStroke(line.width,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND)); for(i<-0 until p.length-1) g.drawLine(p(i).x,p(i).y,p(i+1).x,p(i+1).y) } }
// Crystal labels are screen-space annotations above the 3D gems.
crystals.filter(!_.collected).foreach { c => project(V3(c.x,terrainHeight(c.x,c.z)+2.5,c.z),cam,f,r,u,w,h).foreach { p => if(p.x>0&&p.x<w&&p.y>0&&p.y<h){ g.setFont(new Font("SansSerif",Font.BOLD,10)); g.setColor(colorHue(c.hue)); g.drawString(c.label,p.x-28,p.y) } } }
particles.foreach { p => project(V3(p.x,p.y,p.z),cam,f,r,u,w,h).foreach { q => val alpha=clamp(p.life/1.7,0,1).toFloat; val old=g.getComposite; g.setComposite(AlphaComposite.SrcOver.derive(alpha)); g.setColor(colorHue(p.hue)); val size=math.max(2,(p.size*math.max(0.35,8.0/q.depth)*12).toInt); g.fillOval(q.x-size/2,q.y-size/2,size,size); g.setComposite(old) } }
}
def drawMiniMap(g: Graphics2D,w:Int): Unit = {
val x=w-190; val y=105; val mw=170; val mh=120; g.setColor(new Color(4,12,28,220)); g.fillRoundRect(x,y,mw,mh,14,14); g.setColor(CYAN); g.drawRoundRect(x,y,mw,mh,14,14)
def mx(v:Double)=x+8+(v/WORLD_W*(mw-16)).toInt; def mz(v:Double)=y+8+(v/WORLD_D*(mh-16)).toInt
crystals.filter(!_.collected).foreach(c=>{g.setColor(colorHue(c.hue));g.fillOval(mx(c.x)-2,mz(c.z)-2,5,5)})
guards.foreach(v=>{g.setColor(PINK);g.fillOval(mx(v.x)-3,mz(v.z)-3,6,6)})
g.setColor(MINT);g.fillOval(mx(portalX)-4,mz(portalZ)-4,8,8);g.setColor(WHITE);g.fillOval(mx(playerX)-4,mz(playerZ)-4,8,8)
g.setFont(new Font("SansSerif",Font.BOLD,9));g.drawString("3D REALM MAP",x+9,y+13)
}
def drawHUD(g:Graphics2D,w:Int,h:Int):Unit={
g.setColor(new Color(4,13,31,220));g.fillRoundRect(14,13,445,105,16,16);g.setColor(new Color(79,229,219,180));g.drawRoundRect(14,13,445,105,16,16)
g.setFont(new Font("SansSerif",Font.BOLD,17));g.setColor(CYAN);g.drawString("BIXUU / SPRING SEASON 3D",27,39)
g.setFont(new Font("SansSerif",Font.BOLD,11));g.setColor(WHITE);g.drawString("REALM: "+levels(activeLevel).name,27,60)
g.setColor(GOLD);g.drawString("CRYSTALS: "+collectedCount+" / "+crystals.length+" COINS: "+coins,27,80)
g.setColor(PINK);g.drawString("HEARTS: "+("? "*health),27,100)
val mm=remainingSeconds.toInt/60;val ss=remainingSeconds.toInt%60;g.setColor(new Color(4,13,31,220));g.fillRoundRect(w-160,15,145,57,12,12);g.setFont(new Font("Monospaced",Font.BOLD,21));g.setColor(if(remainingSeconds<40)PINK else GOLD);g.drawString("%02d:%02d".format(mm,ss),w-142,43);g.setFont(new Font("SansSerif",Font.PLAIN,9));g.setColor(WHITE);g.drawString("TIME LEFT",w-140,60)
if(showMiniMap)drawMiniMap(g,w)
g.setColor(new Color(4,13,31,215));g.fillRoundRect(14,h-39,w-28,27,10,10);g.setFont(new Font("SansSerif",Font.BOLD,10));g.setColor(MINT);g.drawString("WASD MOVE SHIFT SPRINT SPACE DASH E COLLECT M MAP F GLOW ESC PAUSE R MENU",25,h-21)
g.setColor(CYAN);g.drawString(multiplayerMode+" | "+currentUser+" | "+weatherMode, w-385,h-21)
if(toastTime>0){val tw=math.min(w-40,math.max(320,g.getFontMetrics.stringWidth(toast)+34));val tx=(w-tw)/2;g.setColor(new Color(4,14,32,235));g.fillRoundRect(tx,h-83,tw,30,12,12);g.setColor(CYAN);g.drawRoundRect(tx,h-83,tw,30,12,12);g.setColor(WHITE);g.setFont(new Font("SansSerif",Font.BOLD,11));g.drawString(toast,tx+15,h-64)}
}
def drawOverlay(g:Graphics2D,w:Int,h:Int):Unit={g.setColor(new Color(3,8,20,205));g.fillRect(0,0,w,h);val title=if(won)"REALM RESTORED!" else if(lost)"ADVENTURE OVER" else "PAUSED";g.setFont(new Font("SansSerif",Font.BOLD,38));g.setColor(if(won)MINT else if(lost)PINK else CYAN);g.drawString(title,w/2-g.getFontMetrics.stringWidth(title)/2,h/2-35);g.setFont(new Font("SansSerif",Font.PLAIN,15));g.setColor(WHITE);val sub=if(won)"Spring energy restored. Great work, "+currentUser+"!" else if(lost)"Try again and avoid the Shadow Guards." else "Your 3D adventure is paused.";g.drawString(sub,w/2-g.getFontMetrics.stringWidth(sub)/2,h/2);g.setFont(new Font("SansSerif",Font.BOLD,14));g.setColor(GOLD);val hint=if(won)"ENTER: NEXT REALM R: LEVEL SELECT"else"ESC: RESUME R: RESTART / LEVEL SELECT";g.drawString(hint,w/2-g.getFontMetrics.stringWidth(hint)/2,h/2+42)}
final class GamePanel extends JPanel {
setPreferredSize(new Dimension(1180,760));setFocusable(true);setDoubleBuffered(true);setBackground(DARK)
addKeyListener(new KeyAdapter {
override def keyPressed(e: KeyEvent): Unit = {
val k = e.getKeyCode
val fresh = !pressedKeys.contains(k)
pressedKeys += k
if (fresh) {
if (k == KeyEvent.VK_E) {
collectOrUse()
} else if (k == KeyEvent.VK_SPACE && started && !won && !lost && dashCooldown <= 0.0) {
dashTime = 0.34
dashCooldown = 1.1
} else if (k == KeyEvent.VK_F) {
showGlow = !showGlow
} else if (k == KeyEvent.VK_M) {
showMiniMap = !showMiniMap
} else if (k == KeyEvent.VK_ESCAPE && !won && !lost) {
paused = !paused
} else if (k == KeyEvent.VK_R) {
if (won || lost) returnToLevels()
else if (paused) paused = false
else returnToLevels()
} else if (k == KeyEvent.VK_ENTER && won) {
if (activeLevel < levels.length - 1) startGame(activeLevel + 1, "SOLO")
else returnToLevels()
}
}
}
override def keyReleased(e: KeyEvent): Unit = {
pressedKeys -= e.getKeyCode
}
})
addMouseListener(new MouseAdapter{override def mousePressed(e:MouseEvent):Unit={mouseLastX=e.getX;mouseLastY=e.getY;requestFocusInWindow()};override def mouseReleased(e:MouseEvent):Unit={mouseLastX = -1;mouseLastY = -1}})
addMouseMotionListener(new MouseMotionAdapter{override def mouseDragged(e:MouseEvent):Unit={if(mouseLastX>=0){val dx=e.getX-mouseLastX;cameraYaw+=dx*0.006;mouseLastX=e.getX;mouseLastY=e.getY;repaint()}}})
override protected def paintComponent(graphics:Graphics):Unit={super.paintComponent(graphics);val g=graphics.create().asInstanceOf[Graphics2D];try{g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);draw3DWorld(g,getWidth,getHeight);drawHUD(g,getWidth,getHeight);if(paused||won||lost)drawOverlay(g,getWidth,getHeight)}finally g.dispose()}
}
// ------------------------- Login screens -------------------------
def loginChoiceScreen():JPanel={val p=verticalPanel();p.add(Box.createVerticalGlue());addCentered(p,heading("BIXUU",25,MINT),4);addCentered(p,heading("SPRING SEASON",42,CYAN),5);addCentered(p,heading("FIVE ELEMENTS / 3D ADVENTURE",17,PINK),3);addCentered(p,bodyLabel("Choose your separate login type."),22);val player=styledButton("PLAYER LOGIN",new Color(15,139,124),MINT);val admin=styledButton("ADMIN LOGIN",new Color(115,61,160),PINK);addCentered(p,player,9);addCentered(p,admin,9);p.add(Box.createVerticalGlue());addAction(player){showCard("player")};addAction(admin){showCard("adminLogin")};p}
def playerLoginScreen():JPanel={val p=verticalPanel();p.add(Box.createVerticalGlue());addCentered(p,heading("PLAYER LOGIN",31,CYAN),8);addCentered(p,bodyLabel("Enter any player name to enter the Spring World."),12);playerNameField=new JTextField("Player",20);fieldStyle(playerNameField);playerNameField.setMaximumSize(new Dimension(300,38));addCentered(p,playerNameField,7);val enter=styledButton("ENTER AS PLAYER",new Color(15,139,124),MINT);val back=styledButton("BACK",new Color(47,71,103),CYAN);addCentered(p,enter,14);addCentered(p,back,7);p.add(Box.createVerticalGlue());addAction(enter){currentUser=safeName(playerNameField.getText);currentRole="PLAYER";isAdmin=false;isSuperAdmin=false;updateWelcome();adminButton.setVisible(false);showCard("menu")};addAction(back){showCard("login")};p}
def adminLoginScreen():JPanel={val p=verticalPanel();p.add(Box.createVerticalGlue());addCentered(p,heading("ADMIN LOGIN",31,GOLD),8);addCentered(p,bodyLabel("Admin access is separate from Player Login."),12);adminNameChoice=new JComboBox[String](Array("Bixuu Admin","Admin 2"));adminNameChoice.setMaximumSize(new Dimension(300,34));addCentered(p,adminNameChoice,7);adminPasswordField=new JPasswordField(20);fieldStyle(adminPasswordField);adminPasswordField.setMaximumSize(new Dimension(300,38));addCentered(p,adminPasswordField,7);val enter=styledButton("VERIFY ADMIN & ENTER",new Color(119,72,165),PINK);val back=styledButton("BACK",new Color(47,71,103),CYAN);addCentered(p,enter,14);addCentered(p,back,7);p.add(Box.createVerticalGlue());addAction(enter){val selected=adminNameChoice.getSelectedIndex;val pin=new String(adminPasswordField.getPassword);val ok=if(selected==0)pin==ADMIN1_PIN else pin==ADMIN2_PIN;if(ok){currentUser=if(selected==0)ADMIN1_NAME else ADMIN2_NAME;currentRole=if(selected==0)"SUPER ADMIN"else"ADMIN 2";isAdmin=true;isSuperAdmin=selected==0;adminPasswordField.setText("");updateWelcome();adminButton.setVisible(true);showCard("menu")}else{JOptionPane.showMessageDialog(frame,"Admin password is incorrect.","Access denied",JOptionPane.ERROR_MESSAGE);adminPasswordField.setText("")}};addAction(back){showCard("login")};p}
def updateWelcome():Unit=if(welcomeLabel!=null)welcomeLabel.setText("Welcome, "+currentUser+" / "+currentRole)
def mainMenuScreen():JPanel={val p=verticalPanel();p.add(Box.createVerticalGlue());addCentered(p,heading("BIXUU SPRING SEASON 3D",31,CYAN),4);welcomeLabel=bodyLabel("Welcome, Player");p.add(welcomeLabel);val levels=styledButton("FIVE REALMS / ADVENTURES",new Color(13,137,112),MINT);val multi=styledButton("MULTIPLAYER - HOTSPOT / WI-FI",new Color(21,103,151),CYAN);adminButton=styledButton("ADMIN CONTROL CENTER",new Color(119,72,165),PINK);val logout=styledButton("LOG OUT",new Color(119,53,70),new Color(255,151,163));addCentered(p,levels,18);addCentered(p,multi,9);addCentered(p,adminButton,9);addCentered(p,logout,9);adminButton.setVisible(false);p.add(Box.createVerticalGlue());addAction(levels){refreshLevelButtons();showCard("levels")};addAction(multi){showCard("multiplayer")};addAction(adminButton){refreshAdminPanel();showCard("admin")};addAction(logout){closeNetwork();if(gameTimer!=null)gameTimer.stop();currentUser="Player";currentRole="PLAYER";isAdmin=false;isSuperAdmin=false;adminButton.setVisible(false);showCard("login")};p}
def levelSelectScreen():JPanel={val p=verticalPanel();addCentered(p,heading("CHOOSE YOUR SPRING REALM",29,CYAN),5);addCentered(p,bodyLabel("Collect every crystal, avoid Shadow Guards and reach the portal."),5);levelButtons.clear();levels.zipWithIndex.foreach{case(l,i)=>val b=styledButton("REALM "+(i+1)+" - "+l.name,l.accent.darker(),l.accent);levelButtons+=b;addCentered(p,b,7);addCentered(p,bodyLabel(l.subtitle),2);addAction(b){if(i==0||completedLevels.contains(i-1))startGame(i,"SOLO")else JOptionPane.showMessageDialog(frame,"Complete the previous Realm first.")}};val back=styledButton("BACK TO MENU",new Color(47,71,103),CYAN);addCentered(p,back,12);p.add(Box.createVerticalGlue());addAction(back){showCard("menu")};refreshLevelButtons();p}
def refreshLevelButtons():Unit={if(levelButtons.length==levels.length)levelButtons.zipWithIndex.foreach{case(b,i)=>b.setEnabled(i==0||completedLevels.contains(i-1));if(i > 0 && !b.isEnabled)b.setText("LOCKED - "+levels(i).name)}}
def labelFor(s:String):JLabel={val l=new JLabel(s);l.setForeground(WHITE);l.setFont(new Font("SansSerif",Font.BOLD,12));l}
// ------------------------- Multiplayer lobby -------------------------
def multiplayerScreen():JPanel={val p=verticalPanel();addCentered(p,heading("MULTIPLAYER LOBBY",30,CYAN),4);addCentered(p,bodyLabel("LAN play: host one room, then friends join using the same Wi-Fi / hotspot."),4);hostIpLabel=bodyLabel("Host IP will appear here after hosting.");addCentered(p,hostIpLabel,10);val portField=new JTextField(DEFAULT_PORT.toString,7);fieldStyle(portField);val roomLevel=new JComboBox[String](levels.map(_.name).toArray);val hostRow=new JPanel();hostRow.setOpaque(false);hostRow.add(labelFor("Port"));hostRow.add(portField);hostRow.add(labelFor("Realm"));hostRow.add(roomLevel);p.add(hostRow);val hostBtn=styledButton("HOST ROOM",new Color(14,135,106),MINT);addCentered(p,hostBtn,8);val ipField=new JTextField("192.168.1.2",14);fieldStyle(ipField);val joinPort=new JTextField(DEFAULT_PORT.toString,7);fieldStyle(joinPort);val joinRow=new JPanel();joinRow.setOpaque(false);joinRow.add(labelFor("Host IPv4"));joinRow.add(ipField);joinRow.add(labelFor("Port"));joinRow.add(joinPort);p.add(joinRow);val joinBtn=styledButton("JOIN ROOM",new Color(21,102,154),CYAN);addCentered(p,joinBtn,8);networkStatus=bodyLabel("Status: not connected");addCentered(p,networkStatus,6);val back=styledButton("BACK TO MENU",new Color(47,71,103),CYAN);addCentered(p,back,8);p.add(Box.createVerticalGlue());addAction(hostBtn){try{val port=portField.getText.trim.toInt;require(port>=1024&&port<=65535,"Port must be between 1024 and 65535.");closeNetwork();val server=new LanServer(port,currentUser,roomLevel.getSelectedIndex);server.start();lanServer=Some(server);val ip=localIPv4;hostIpLabel.setText("HOST IP: "+ip+" PORT: "+port);networkStatus.setText("Room hosted - waiting for players.");JOptionPane.showMessageDialog(frame,"Room hosted. Share this IP and port with friends:\n\n"+ip+":"+port+"\n\nAllow Java through Windows Firewall on Private networks.");startGame(roomLevel.getSelectedIndex,"HOST")}catch{case ex:Throwable=>JOptionPane.showMessageDialog(frame,"Could not host: "+ex.getMessage,"Host error",JOptionPane.ERROR_MESSAGE)}};addAction(joinBtn){try{val ip=ipField.getText.trim;val port=joinPort.getText.trim.toInt;require(ip.nonEmpty,"Enter the host IPv4 address.");closeNetwork();val client=new LanClient(ip,port,currentUser,roomLevel.getSelectedIndex);lanClient=Some(client);networkStatus.setText("Connected to "+ip+":"+port);startGame(roomLevel.getSelectedIndex,"JOIN")}catch{case ex:Throwable=>JOptionPane.showMessageDialog(frame,"Could not join: "+ex.getMessage+"\nCheck IP, port, same Wi-Fi and firewall.","Join error",JOptionPane.ERROR_MESSAGE)}};addAction(back){showCard("menu")};p}
// ------------------------- Admin control center -------------------------
def refreshAdminPanel():Unit={if(eventListArea!=null)eventListArea.setText((Vector("BUILT-IN: GLITCH IN THE GARDEN","BUILT-IN: "+activeEventTitle)++extraEvents.map("CUSTOM: "+_)).mkString("\n"));if(eventTitleField!=null)eventTitleField.setText(activeEventTitle)}
def adminScreen():JPanel={val p=verticalPanel();addCentered(p,heading("ADMIN CONTROL CENTER",28,GOLD),4);addCentered(p,bodyLabel("Role: SUPER ADMIN has full local controls. Admin 2 has event controls."),4);val erow=new JPanel();erow.setOpaque(false);erow.add(labelFor("Event title"));eventTitleField=new JTextField(activeEventTitle,27);fieldStyle(eventTitleField);erow.add(eventTitleField);p.add(erow);val save=styledButton("SAVE EVENT TITLE",new Color(119,72,165),PINK);addCentered(p,save,7);val addrow=new JPanel();addrow.setOpaque(false);eventAddField=new JTextField(24);fieldStyle(eventAddField);addrow.add(labelFor("New event"));addrow.add(eventAddField);p.add(addrow);val add=styledButton("ADD EVENT",new Color(13,137,112),MINT);addCentered(p,add,7);val arow=new JPanel();arow.setOpaque(false);announcementField=new JTextField(25);fieldStyle(announcementField);arow.add(labelFor("Announcement"));arow.add(announcementField);p.add(arow);val announce=styledButton("BROADCAST ANNOUNCEMENT",new Color(21,102,154),CYAN);addCentered(p,announce,7);val wr=new JPanel();wr.setOpaque(false);weatherChoice=new JComboBox[String](Array("SPRING BREEZE","SUNNY BLOOM","RAINY MONSOON","AURORA NIGHT","STARFALL"));wr.add(labelFor("World weather"));wr.add(weatherChoice);p.add(wr);val applyWeather=styledButton("APPLY WORLD WEATHER",new Color(14,135,106),MINT);addCentered(p,applyWeather,7);eventListArea=new JTextArea(5,46);eventListArea.setEditable(false);eventListArea.setFont(new Font("Monospaced",Font.PLAIN,12));eventListArea.setBackground(new Color(4,13,30));eventListArea.setForeground(MINT);val sc=new JScrollPane(eventListArea);sc.setAlignmentX(Component.CENTER_ALIGNMENT);p.add(Box.createVerticalStrut(8));p.add(sc);val back=styledButton("BACK TO MENU",new Color(47,71,103),CYAN);addCentered(p,back,9);p.add(Box.createVerticalGlue());addAction(save){val v=eventTitleField.getText.trim;if(v.nonEmpty){activeEventTitle=v.take(64);refreshAdminPanel();JOptionPane.showMessageDialog(frame,"Event title updated for this session.")}};addAction(add){val v=eventAddField.getText.trim;if(v.nonEmpty && !extraEvents.contains(v)){extraEvents+=v.take(64);eventAddField.setText("");refreshAdminPanel()}};addAction(announce){val v=announcementField.getText.trim;if(v.nonEmpty){announcement=v.take(110);announcementField.setText("");showToast("ANNOUNCEMENT: "+announcement,5.0);JOptionPane.showMessageDialog(frame,"Announcement set: "+announcement)}};addAction(applyWeather){weatherMode=weatherChoice.getSelectedItem.toString;showToast("WORLD WEATHER: "+weatherMode,4.0)};addAction(back){showCard("menu")};refreshAdminPanel();p}
def buildUI():Unit={
cardLayout=new CardLayout();root=new JPanel(cardLayout);root.add(loginChoiceScreen(),"login");root.add(playerLoginScreen(),"player");root.add(adminLoginScreen(),"adminLogin");root.add(mainMenuScreen(),"menu");root.add(levelSelectScreen(),"levels");root.add(multiplayerScreen(),"multiplayer");root.add(adminScreen(),"admin")
frame=new JFrame(APP_TITLE);frame.setContentPane(root);frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);frame.addWindowListener(new WindowAdapter{override def windowClosing(e:WindowEvent):Unit={if(gameTimer!=null)gameTimer.stop();closeNetwork();frame.dispose()}})
frame.setMinimumSize(new Dimension(920,680));frame.setSize(1180,780);frame.setLocationRelativeTo(null);frame.setVisible(true);showCard("login")
}
def main(args:Array[String]):Unit=SwingUtilities.invokeLater(new Runnable{override def run():Unit=buildUI()})
}
BixuuSpringSeasonMultiplayer.main(Array[String]())