Code Sketch
yoixx
Category: Programming
import java.awt.{AlphaComposite, BasicStroke, BorderLayout, Color, Component, Dimension, Font, GradientPaint, Graphics, Graphics2D, RenderingHints}
import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, WindowAdapter, WindowEvent}
import java.io.{BufferedReader, InputStreamReader, PrintWriter}
import java.net.{InetAddress, InetSocketAddress, ServerSocket, Socket}
import java.util.concurrent.{ConcurrentHashMap, ConcurrentLinkedQueue}
import java.util.concurrent.atomic.AtomicInteger
import java.awt.geom.{Ellipse2D, Path2D}
import javax.swing.{BorderFactory, Box, BoxLayout, JButton, JComboBox, JComponent, JFrame, JLabel, JOptionPane, JPanel, JPasswordField, JScrollPane, JTextArea, JTextField, SwingConstants, SwingUtilities, Timer, WindowConstants}
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
/**
* SPRING SEASON - FOURTH EVENT
* SANCTUS EVENT
* Stand-alone Scala + Java Swing game. No external assets or libraries required.
* Admin account: BIXUU@GC. The existing PIN is checked but never displayed in the UI.
*/
object SanctusFourthEvent {
// ---------------------------------------------------------------------------
// APPLICATION SETTINGS
// ---------------------------------------------------------------------------
private val APP_TITLE = "SANCTUS EVENT | SPRING SEASON 04"
private val ADMIN_NAME = "BIXUU@GC"
// Retains the PIN supplied in the earlier project; it is intentionally not shown in the UI.
private val ADMIN_PIN = "BIXUU-2026"
private val WORLD_W = 1900.0
private val WORLD_H = 1320.0
private val PORTAL_X = 1710.0
private val PORTAL_Y = 175.0
private val SPAWN_X = 135.0
private val SPAWN_Y = 1130.0
private val C_BG = new Color(7, 12, 29)
private val C_PANEL = new Color(13, 25, 48)
private val C_PANEL_2 = new Color(20, 39, 65)
private val C_LINE = new Color(74, 117, 150)
private val C_WHITE = new Color(241, 248, 255)
private val C_SOFT = new Color(176, 198, 220)
private val C_MINT = new Color(116, 245, 205)
private val C_GOLD = new Color(255, 217, 133)
private val C_PINK = new Color(255, 145, 205)
private val C_BLUE = new Color(108, 194, 255)
private val C_VIOLET = new Color(183, 143, 255)
private val DIFFICULTIES = Array("EASY", "MEDIUM", "HARD", "EXTREME", "NIGHTMARE")
private val GAME_MODES = Array("ADVENTURE", "TIME ATTACK", "GEM HUNT", "FREE ROAM")
private val NETWORK_PORT = 5050
private final case class GemInfo(id: Int, name: String, powerName: String, tint: Color, description: String)
private val POWER_GEMS: Vector[GemInfo] = Vector(
GemInfo(0, "RUBY CORE", "Flame Arc", new Color(255, 76, 91), "A focused blast that damages nearby enemies."),
GemInfo(1, "SAPPHIRE CORE", "Frost Lock", new Color(75, 190, 255), "Freezes nearby enemies for a short time."),
GemInfo(2, "EMERALD CORE", "Life Bloom", new Color(81, 226, 142), "Restores health and one charge."),
GemInfo(3, "AMETHYST CORE", "Nova Burst", new Color(190, 112, 255), "Releases a powerful ring of energy."),
GemInfo(4, "TOPAZ CORE", "Time Bend", new Color(255, 205, 76), "Slows every enemy for a few seconds.")
)
// Use Vector values before indexing; direct Array(...)(index) can be parsed as
// a second argument list for Array.apply on some Scala/Kojo compiler versions.
private val RELIC_NAMES: Vector[String] = Vector(
"LUMEN SIGIL", "ORBIT GLASS", "DAWN SEAL",
"AETHER MARK", "SOLAR TOKEN", "VEIL KEY"
)
private val DIFFICULTY_TIME: Vector[Int] = Vector(100, 35, -20, -65, -105)
private final case class RealmInfo(name: String, caption: String, relics: Int, sentries: Int, hue: Int, time: Int)
private val realms = Array(
RealmInfo("MOSSWIND ATRIUM", "A quiet maze of living stone and lantern vines", 5, 1, 145, 360),
RealmInfo("OPAL TIDEWAY", "A glass coast above a slow-moving sky sea", 6, 2, 190, 345),
RealmInfo("SOLARIS CANOPY", "A high garden suspended around a sunlit engine", 7, 3, 42, 330),
RealmInfo("VIOLET SKYFORGE", "A floating foundry where the air carries light", 8, 3, 270, 315),
RealmInfo("SANCTUM OF FIRST LIGHT", "The final chamber beyond the luminous veil", 9, 4, 315, 300)
)
// ---------------------------------------------------------------------------
// GAME OBJECTS
// ---------------------------------------------------------------------------
private final class Relic(val x: Double, val y: Double, val tint: Color, val title: String) {
var collected = false
}
private final class GemDrop(val x: Double, val y: Double, val gemId: Int) {
var collected = false
}
private final class Sentry(
val baseX: Double,
val baseY: Double,
val radius: Double,
val pace: Double,
val phase: Double,
val tint: Color,
val maxHealth: Int,
val rank: Int
) {
var x = baseX
var y = baseY
var disabled = 0.0
var slowTime = 0.0
var health = maxHealth
var defeated = false
var hitFlash = 0.0
}
private final case class NetPlayer(id: String, name: String, x: Double, y: Double, hp: Int, realm: Int)
/** LAN room server. The room host owns the world; authenticated Admin clients may issue controller commands. */
private final class LanServer(port: Int, hostName: String) {
private val socket = new ServerSocket()
private val running = new java.util.concurrent.atomic.AtomicBoolean(true)
private val counter = new AtomicInteger(0)
private val writers = new ConcurrentHashMap[String, PrintWriter]()
private val positions = new ConcurrentHashMap[String, String]()
private val adminIds = new ConcurrentHashMap[String, String]()
private val hostCommands = new ConcurrentLinkedQueue[String]()
private val hostActions = new ConcurrentLinkedQueue[String]()
@volatile private var roomRealm = 1
@volatile private var roomDifficulty = "EASY"
@volatile private var roomMode = "ADVENTURE"
socket.setReuseAddress(true)
socket.bind(new InetSocketAddress(port))
positions.put("HOST", safeNetworkName(hostName) + ",135.0,1130.0,100,1")
def start(): Unit = {
val acceptThread = new Thread(new Runnable {
override def run(): Unit = {
while (running.get()) {
try {
val client = socket.accept()
client.setTcpNoDelay(true)
val worker = new Thread(new Runnable { override def run(): Unit = handleClient(client) })
worker.setDaemon(true)
worker.start()
} catch {
case _: java.net.SocketException => ()
case _: Throwable => ()
}
}
}
})
acceptThread.setDaemon(true)
acceptThread.start()
}
private def handleClient(client: Socket): Unit = {
var id = ""
var output: PrintWriter = null
try {
val input = new BufferedReader(new InputStreamReader(client.getInputStream, "UTF-8"))
output = new PrintWriter(client.getOutputStream, true)
val first = input.readLine()
if (first != null && first.startsWith("JOIN|")) {
val fields = first.split("\\|", 3)
val requestedName = safeNetworkName(if (fields.length > 1) fields(1) else "PLAYER")
val token = if (fields.length > 2) fields(2) else ""
id = "P" + counter.incrementAndGet()
val authorized = requestedName.equalsIgnoreCase(ADMIN_NAME) && token == ADMIN_PIN
writers.put(id, output)
positions.put(id, requestedName + ",135.0,1130.0,100,1")
if (authorized) adminIds.put(id, "1")
output.println("WELCOME|" + id + "|" + (if (authorized) "ADMIN" else "PLAYER"))
output.println("WORLD|" + roomRealm + "|" + roomDifficulty + "|" + roomMode)
broadcastState()
var line = input.readLine()
while (line != null && running.get()) {
if (line.startsWith("POS|")) {
val values = line.substring(4).split(",", 4)
if (values.length == 4) {
try {
val old = positions.get(id)
val name = if (old == null) requestedName else old.takeWhile(_ != ',')
val px = clamp(values(0).toDouble, 0.0, WORLD_W)
val py = clamp(values(1).toDouble, 0.0, WORLD_H)
val hp = math.max(0, math.min(100, values(2).toInt))
val realm = math.max(1, math.min(realms.length, values(3).toInt))
positions.put(id, name + "," + px + "," + py + "," + hp + "," + realm)
broadcastState()
} catch { case _: Throwable => () }
}
} else if (line.startsWith("ADMIN|") && adminIds.containsKey(id)) {
val fields = line.split("\\|", 3)
if (fields.length == 3) publishAdminCommand(fields(1), fields(2))
} else if (line.startsWith("ACT|")) {
val action = line.substring(4).take(100).replaceAll("[\\r\\n]", "_")
hostActions.add(action)
broadcastExcept(id, "ACT|" + action)
}
line = input.readLine()
}
}
} catch { case _: Throwable => () }
finally {
if (id.nonEmpty) {
writers.remove(id)
positions.remove(id)
adminIds.remove(id)
broadcastState()
}
try client.close() catch { case _: Throwable => () }
}
}
private def broadcast(message: String): Unit = {
val it = writers.values().iterator()
while (it.hasNext) {
try { val out = it.next(); out.synchronized { out.println(message) } }
catch { case _: Throwable => () }
}
}
private def broadcastExcept(exceptId: String, message: String): Unit = {
val it = writers.entrySet().iterator()
while (it.hasNext) {
val entry = it.next()
if (entry.getKey != exceptId) {
try { val out = entry.getValue; out.synchronized { out.println(message) } }
catch { case _: Throwable => () }
}
}
}
private def broadcastState(): Unit = {
val b = new StringBuilder("STATE|")
val it = positions.entrySet().iterator()
var first = true
while (it.hasNext) {
val e = it.next()
if (!first) b.append(";")
first = false
b.append(e.getKey).append(",").append(e.getValue)
}
broadcast(b.toString)
}
private def publishAdminCommand(command: String, target: String): Unit = {
val safe = command.replaceAll("[|;\\r\\n]", "_")
val who = target.replaceAll("[|;\\r\\n]", "_")
val wire = safe + "|" + who
hostCommands.add(wire)
broadcast("CMD|" + wire)
}
def publishHostPosition(x: Double, y: Double, hp: Int, realm: Int, difficulty: String, mode: String): Unit = {
roomRealm = math.max(1, math.min(realms.length, realm))
roomDifficulty = if (DIFFICULTIES.contains(difficulty)) difficulty else "EASY"
roomMode = if (GAME_MODES.contains(mode)) mode else "ADVENTURE"
positions.put("HOST", safeNetworkName(hostName) + "," + x + "," + y + "," + hp + "," + roomRealm)
broadcastState()
broadcast("WORLD|" + roomRealm + "|" + roomDifficulty + "|" + roomMode)
}
def broadcastHostCommand(command: String, target: String): Unit = publishAdminCommand(command, target)
def consumeHostCommands(): Seq[String] = {
val b = ArrayBuffer[String]()
var value = hostCommands.poll()
while (value != null) { b += value; value = hostCommands.poll() }
b.toSeq
}
def broadcastHostAction(action: String): Unit = broadcast("ACT|" + action.take(100).replaceAll("[\\r\\n]", "_"))
def consumeHostActions(): Seq[String] = {
val b = ArrayBuffer[String]()
var value = hostActions.poll()
while (value != null) { b += value; value = hostActions.poll() }
b.toSeq
}
def snapshot(): Seq[NetPlayer] = parseNetworkSnapshot(positions)
def close(): Unit = {
running.set(false)
try socket.close() catch { case _: Throwable => () }
val it = writers.values().iterator()
while (it.hasNext) try it.next().close() catch { case _: Throwable => () }
writers.clear(); positions.clear(); adminIds.clear(); hostActions.clear(); hostCommands.clear()
}
}
/** LAN room client. Admin proof is only sent when the Admin has logged in locally. */
private final class LanClient(host: String, port: Int, name: String, adminToken: String) {
private val socket = new Socket()
socket.connect(new InetSocketAddress(host, port), 3500)
socket.setTcpNoDelay(true)
private val output = new PrintWriter(socket.getOutputStream, true)
private val input = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
private val peers = new ConcurrentHashMap[String, String]()
private val commands = new ConcurrentLinkedQueue[String]()
private val actions = new ConcurrentLinkedQueue[String]()
@volatile private var connected = true
@volatile private var localId = ""
@volatile private var serverConfirmedAdmin = false
@volatile private var roomRealm = 1
@volatile private var roomDifficulty = "EASY"
@volatile private var roomMode = "ADVENTURE"
output.println("JOIN|" + safeNetworkName(name) + "|" + adminToken)
private val welcome = input.readLine()
if (welcome == null || !welcome.startsWith("WELCOME|")) throw new java.io.IOException("The room did not accept the connection.")
val welcomeParts = welcome.split("\\|", 3)
localId = if (welcomeParts.length > 1) welcomeParts(1) else ""
serverConfirmedAdmin = welcomeParts.length > 2 && welcomeParts(2) == "ADMIN"
private val reader = new Thread(new Runnable {
override def run(): Unit = {
try {
var line = input.readLine()
while (line != null && connected) {
if (line.startsWith("STATE|")) {
peers.clear()
val body = line.substring(6)
if (body.nonEmpty) body.split(";").foreach { entry =>
val parts = entry.split(",", 6)
if (parts.length == 6) peers.put(parts(0), parts.drop(1).mkString(","))
}
} else if (line.startsWith("CMD|")) commands.add(line.substring(4))
else if (line.startsWith("ACT|")) actions.add(line.substring(4))
else if (line.startsWith("WORLD|")) {
val cfg = line.split("\\|", 4)
if (cfg.length == 4) {
try {
val r = cfg(1).toInt
if (r >= 1 && r <= realms.length) roomRealm = r
if (DIFFICULTIES.contains(cfg(2))) roomDifficulty = cfg(2)
if (GAME_MODES.contains(cfg(3))) roomMode = cfg(3)
} catch { case _: Throwable => () }
}
}
line = input.readLine()
}
} catch { case _: Throwable => () }
connected = false
}
})
reader.setDaemon(true)
reader.start()
def id: String = localId
def worldConfig: (Int, String, String) = (roomRealm, roomDifficulty, roomMode)
def isAdminOnServer: Boolean = serverConfirmedAdmin
def isConnected: Boolean = connected
def sendPosition(x: Double, y: Double, hp: Int, realm: Int): Unit = if (connected) output.println("POS|" + x + "," + y + "," + hp + "," + realm)
def sendAdminCommand(command: String, target: String): Unit = if (connected && serverConfirmedAdmin) output.println("ADMIN|" + command + "|" + target)
def sendAction(action: String): Unit = if (connected) output.println("ACT|" + action.take(100).replaceAll("[\\r\\n]", "_"))
def consumeActions(): Seq[String] = {
val b = ArrayBuffer[String]()
var value = actions.poll()
while (value != null) { b += value; value = actions.poll() }
b.toSeq
}
def consumeCommands(): Seq[String] = {
val b = ArrayBuffer[String]()
var value = commands.poll()
while (value != null) { b += value; value = commands.poll() }
b.toSeq
}
def snapshot(): Seq[NetPlayer] = parseNetworkSnapshot(peers)
def close(): Unit = { connected = false; try socket.close() catch { case _: Throwable => () }; actions.clear(); commands.clear() }
}
private final class Bloom(val x: Double, val y: Double, val size: Double, val hue: Int, val phase: Double)
private final class Star(val x: Double, val y: Double, val size: Int, val phase: Double)
private final class Spark(var x: Double, var y: Double, var vx: Double, var vy: Double, val size: Double, val tint: Color, var life: Double)
private def safeNetworkName(value: String): String = Option(value).getOrElse("PLAYER").trim.replaceAll("[,;|\\r\\n]", "_").take(18) match {
case "" => "PLAYER"
case other => other
}
private def parseNetworkSnapshot(source: ConcurrentHashMap[String, String]): Seq[NetPlayer] = {
val result = ArrayBuffer[NetPlayer]()
val it = source.entrySet().iterator()
while (it.hasNext) {
val e = it.next()
val parts = e.getValue.split(",", 5)
if (parts.length == 5) {
try result += NetPlayer(e.getKey, parts(0), parts(1).toDouble, parts(2).toDouble, parts(3).toInt, parts(4).toInt)
catch { case _: Throwable => () }
}
}
result.toSeq
}
// ---------------------------------------------------------------------------
// APPLICATION STATE
// ---------------------------------------------------------------------------
private var frame: JFrame = null
private var root: JPanel = null
private var cardLayout: java.awt.CardLayout = null
private var gameCanvas: RealmCanvas = null
private var gameTimer: Timer = null
private var playerName = "PLAYER"
private var isAdmin = false
private var eventTitle = "SANCTUS EVENT"
private var eventSubtitle = "THE LUMEN CIRCUIT"
private var activeRealm = 1
private var selectedDifficulty = "EASY"
private var selectedMode = "ADVENTURE"
private val completedRealms = mutable.Set[Int]()
private val keys = mutable.Set[Int]()
private val relics = ArrayBuffer[Relic]()
private val sentries = ArrayBuffer[Sentry]()
private val gemDrops = ArrayBuffer[GemDrop]()
private val droppedGemIds = mutable.Set[Int]()
private val collectedGems = mutable.Set[Int]()
private val blooms = ArrayBuffer[Bloom]()
private val stars = ArrayBuffer[Star]()
private val sparks = ArrayBuffer[Spark]()
private val rng = new java.util.Random(41042026L)
private var playerX = SPAWN_X
private var playerY = SPAWN_Y
private var cameraX = 0.0
private var cameraY = 0.0
private var facingX = 0.0
private var facingY = -1.0
private var frameCount = 0L
private var secondsLeft = 360.0
private var charge = 4
private var started = false
private var paused = false
private var cleared = false
private var failed = false
private var dashTime = 0.0
private var dashCooldown = 0.0
private var pulseCooldown = 0.0
private var invulnerability = 0.0
private var pulseFlash = 0.0
private var playerHealth = 100
private var attackCooldown = 0.0
private var powerCooldown = 0.0
private var attackFlash = 0.0
private var speedBoostTime = 0.0
private var equippedGem = -1
private var enemyKills = 0
private var multiplayerMode = "SOLO"
private var lanServer: Option[LanServer] = None
private var lanClient: Option[LanClient] = None
private var lastNetworkFrame = 0L
private var collectionFromGame = false
private var collectionWasPaused = false
private var adminFromGame = false
private var adminWasPaused = false
private var collectionButtons = ArrayBuffer[JButton]()
private var collectionStatusArea: JTextArea = null
private var adminTargetBox: JComboBox[String] = null
private var toast = "Find the first enemy, fight with F, and claim its Ruby Core."
private var toastTime = 4.0
private var loginNameField: JTextField = null
private var loginPinField: JPasswordField = null
private var adminMenuButton: JButton = null
private var welcomeLabel: JLabel = null
private var realmButtons = ArrayBuffer[JButton]()
private var adminTitleField: JTextField = null
private var adminSubtitleField: JTextField = null
private var adminDifficultyBox: JComboBox[String] = null
private var adminModeBox: JComboBox[String] = null
private var adminStatusArea: JTextArea = null
private var hostButton: JButton = null
private var joinButton: JButton = null
private var networkAdminTimer: Timer = null
// ---------------------------------------------------------------------------
// GENERAL HELPERS
// ---------------------------------------------------------------------------
private def clamp(v: Double, lo: Double, hi: Double): Double = math.max(lo, math.min(hi, v))
private def distance(x1: Double, y1: Double, x2: Double, y2: Double): Double = {
val dx = x1 - x2
val dy = y1 - y2
math.sqrt(dx * dx + dy * dy)
}
private def worldX(x: Double): Int = math.round(x - cameraX).toInt
private def worldY(y: Double): Int = math.round(y - cameraY).toInt
private def relicCount: Int = relics.count(_.collected)
private def aliveEnemyCount: Int = sentries.count(s => !s.defeated)
private def currentGem: Option[GemInfo] = POWER_GEMS.find(_.id == equippedGem)
private def localNetworkId: String = if (multiplayerMode == "HOST") "HOST" else if (multiplayerMode == "JOIN") lanClient.map(_.id).getOrElse("") else "LOCAL"
private def networkPlayers: Seq[NetPlayer] = multiplayerMode match {
case "HOST" => lanServer.map(_.snapshot()).getOrElse(Seq.empty)
case "JOIN" => lanClient.map(_.snapshot()).getOrElse(Seq.empty)
case _ => Seq.empty
}
private def currentRealm: RealmInfo = realms(activeRealm - 1)
private def colorFromHue(hue: Int, sat: Float = 0.68f, bright: Float = 1.0f): Color = Color.getHSBColor((hue.toDouble / 360.0).toFloat, sat, bright)
private def setToast(message: String, duration: Double = 2.8): Unit = {
toast = message
toastTime = duration
}
private def makeButton(text: String): JButton = {
val b = new JButton(text)
b.setFont(new Font("Dialog", Font.BOLD, 14))
b.setForeground(C_WHITE)
b.setBackground(new Color(24, 59, 83))
b.setFocusPainted(false)
b.setCursor(java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR))
b.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(C_LINE, 1),
BorderFactory.createEmptyBorder(10, 18, 10, 18)
))
b.setAlignmentX(Component.CENTER_ALIGNMENT)
b
}
private def makeTitle(text: String, size: Int, color: Color): JLabel = {
val l = new JLabel(text, SwingConstants.CENTER)
l.setFont(new Font("Dialog", Font.BOLD, size))
l.setForeground(color)
l.setAlignmentX(Component.CENTER_ALIGNMENT)
l
}
private def makeLabel(text: String, size: Int = 13, color: Color = C_SOFT): JLabel = {
val l = new JLabel(text, SwingConstants.CENTER)
l.setFont(new Font("Dialog", Font.PLAIN, size))
l.setForeground(color)
l.setAlignmentX(Component.CENTER_ALIGNMENT)
l
}
private def verticalPanel(): JPanel = {
val p = new JPanel()
p.setOpaque(true)
p.setBackground(C_BG)
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS))
p.setBorder(BorderFactory.createEmptyBorder(22, 28, 22, 28))
p
}
private def addCentered(p: JPanel, c: JComponent, gap: Int = 9): Unit = {
p.add(Box.createVerticalStrut(gap))
c.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(c)
}
private def addAction(button: JButton)(action: => Unit): Unit = {
button.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = action
})
}
private def makeCombo(values: Array[String], selected: String): JComboBox[String] = {
val c = new JComboBox[String](values)
c.setSelectedItem(selected)
c.setFont(new Font("Dialog", Font.BOLD, 13))
c.setForeground(C_WHITE)
c.setBackground(C_PANEL_2)
c
}
private def showCard(name: String): Unit = {
cardLayout.show(root, name)
root.revalidate()
root.repaint()
}
private def scrollCard(panel: JPanel): JScrollPane = {
val s = new JScrollPane(panel)
s.setBorder(BorderFactory.createEmptyBorder())
s.getViewport.setBackground(C_BG)
s.getVerticalScrollBar.setUnitIncrement(18)
s
}
// ---------------------------------------------------------------------------
// LEVEL CONSTRUCTION AND GAME FLOW
// ---------------------------------------------------------------------------
private def startRealm(number: Int, bypassUnlock: Boolean = false): Unit = {
if (number < 1 || number > realms.length) return
if (!bypassUnlock && number > 1 && !completedRealms.contains(number - 1)) {
JOptionPane.showMessageDialog(frame, "Clear the previous realm before opening this gate.", "Gate sealed", JOptionPane.INFORMATION_MESSAGE)
return
}
activeRealm = number
buildRealm()
if (gameTimer != null) gameTimer.stop()
if (gameCanvas != null) root.remove(gameCanvas)
gameCanvas = new RealmCanvas()
root.add(gameCanvas, "game")
showCard("game")
gameTimer = new Timer(16, new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
updateGame(0.016)
if (gameCanvas != null) gameCanvas.repaint()
}
})
gameTimer.start()
gameCanvas.requestFocusInWindow()
}
private def buildRealm(): Unit = {
relics.clear()
sentries.clear()
gemDrops.clear()
droppedGemIds.clear()
blooms.clear()
stars.clear()
sparks.clear()
keys.clear()
val info = currentRealm
val difficultyIndex = DIFFICULTIES.indexOf(selectedDifficulty) match {
case n if n >= 0 => n
case _ => 1
}
val mode = selectedMode
val extraRelics = if (mode == "GEM HUNT") 2 else 0
val totalRelics = info.relics + extraRelics
var i = 0
while (i < totalRelics) {
val x = 285.0 + ((i * 271 + activeRealm * 113) % 1310).toDouble
val y = 260.0 + ((i * 187 + activeRealm * 79) % 810).toDouble
val tint = colorFromHue((info.hue + i * 39 + 35) % 360)
val name: String = RELIC_NAMES(i % RELIC_NAMES.length)
relics += new Relic(x, y, tint, name)
i += 1
}
val noHazards = mode == "FREE ROAM"
val hazardCount = if (noHazards) 0 else math.max(1, info.sentries + math.max(0, difficultyIndex - 1))
i = 0
while (i < hazardCount) {
val x = 470.0 + i * 330.0
val y = 345.0 + (i % 2) * 390.0
val tint = if (i % 2 == 0) C_PINK else C_VIOLET
val hp = 46 + (activeRealm - 1) * 18 + difficultyIndex * 17 + i * 8
val rank = 1 + activeRealm + difficultyIndex
val pace = 0.58 + (activeRealm - 1) * 0.13 + difficultyIndex * 0.11
sentries += new Sentry(x, y, 75.0 + difficultyIndex * 15.0, pace, i * 1.31, tint, hp, rank)
i += 1
}
i = 0
while (i < 250) {
val x = 25.0 + rng.nextDouble() * (WORLD_W - 50.0)
val y = 30.0 + rng.nextDouble() * (WORLD_H - 60.0)
blooms += new Bloom(x, y, 3.0 + rng.nextDouble() * 8.0, (info.hue + rng.nextInt(130)) % 360, rng.nextDouble() * 6.28)
i += 1
}
i = 0
while (i < 180) {
stars += new Star(rng.nextDouble() * WORLD_W, rng.nextDouble() * WORLD_H, 1 + rng.nextInt(3), rng.nextDouble() * 6.28)
i += 1
}
playerX = SPAWN_X
playerY = SPAWN_Y
cameraX = 0.0
cameraY = 0.0
facingX = 0.0
facingY = -1.0
frameCount = 0L
val difficultyTime: Int = DIFFICULTY_TIME(difficultyIndex)
val modeTime = if (mode == "TIME ATTACK") -100 else if (mode == "GEM HUNT") -35 else 0
secondsLeft = if (mode == "FREE ROAM") 99999.0 else math.max(100.0, info.time + difficultyTime + modeTime)
charge = if (difficultyIndex >= 3) 3 else 4
playerHealth = 100
attackCooldown = 0.0
powerCooldown = 0.0
attackFlash = 0.0
speedBoostTime = 0.0
enemyKills = 0
if (equippedGem < 0 && collectedGems.nonEmpty) equippedGem = collectedGems.toSeq.sorted.head
started = true
paused = false
cleared = false
failed = false
dashTime = 0.0
dashCooldown = 0.0
pulseCooldown = 0.0
invulnerability = 0.0
pulseFlash = 0.0
setToast("Realm opened: " + info.name + " | Fight with F. First enemy drops the Ruby Core.", 5.0)
}
private def stopGame(): Unit = {
if (gameTimer != null) gameTimer.stop()
gameTimer = null
started = false
keys.clear()
}
private def returnToRealms(): Unit = {
stopGame()
refreshRealmButtons()
showCard("realms")
}
private def restartRealm(): Unit = {
buildRealm()
if (gameCanvas != null) gameCanvas.requestFocusInWindow()
}
private def dropGem(enemy: Sentry): Unit = {
val next = POWER_GEMS.find(g => !collectedGems.contains(g.id) && !droppedGemIds.contains(g.id))
next.foreach { gem =>
droppedGemIds += gem.id
gemDrops += new GemDrop(enemy.x, enemy.y, gem.id)
burst(enemy.x, enemy.y, gem.tint, 25)
setToast("ENEMY DEFEATED ? " + gem.name + " dropped! Move close and press E.", 3.8)
}
}
private def defeatEnemy(enemy: Sentry): Unit = {
if (!enemy.defeated) {
enemy.defeated = true
enemyKills += 1
dropGem(enemy)
burst(enemy.x, enemy.y, enemy.tint, 32)
}
}
private def damageEnemy(enemy: Sentry, amount: Int): Unit = {
if (!enemy.defeated) {
enemy.health = math.max(0, enemy.health - amount)
enemy.hitFlash = 0.22
burst(enemy.x, enemy.y, enemy.tint, 7)
if (enemy.health <= 0) defeatEnemy(enemy)
}
}
private def fight(): Unit = {
if (!started || paused || cleared || failed || attackCooldown > 0.0) return
attackCooldown = 0.42
attackFlash = 0.20
val target = sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 132.0)
.sortBy(s => distance(playerX, playerY, s.x, s.y)).headOption
target match {
case Some(enemy) =>
val damage = 34 + activeRealm * 3 + (if (speedBoostTime > 0.0) 8 else 0)
sharedDamageEnemy(enemy, damage)
if (enemy.defeated) setToast("ENEMY DEFEATED ? a POWER GEM has dropped. Move close and press E.", 3.0)
else setToast("GAUNTLET STRIKE ? " + damage + " DAMAGE ? ENEMY HP " + enemy.health + "/" + enemy.maxHealth, 1.5)
case None => setToast("No enemy in range. Move closer, then press F to fight.", 1.5)
}
}
private def useEquippedPower(): Unit = {
if (!started || paused || cleared || failed) return
if (equippedGem < 0 || !collectedGems.contains(equippedGem)) {
setToast("NO GEM EQUIPPED ? defeat an enemy and collect the Ruby Core first.", 2.5)
return
}
if (powerCooldown > 0.0) {
setToast("POWER RECHARGING: " + math.ceil(powerCooldown).toInt + "s", 1.3)
return
}
powerCooldown = 7.0
val gem = POWER_GEMS(equippedGem)
gem.id match {
case 0 =>
var hits = 0
sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 300.0).foreach { enemy =>
sharedDamageEnemy(enemy, 55 + activeRealm * 7); hits += 1
}
burst(playerX + facingX * 70.0, playerY + facingY * 70.0, gem.tint, 36)
setToast("RUBY CORE ? FLAME ARC ? " + hits + " ENEMY/ENEMIES HIT", 2.2)
case 1 =>
var frozen = 0
sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 460.0).foreach { enemy => enemy.disabled = 4.5; sendSharedAction("FREEZE|" + sentries.indexOf(enemy) + "|4500"); frozen += 1 }
burst(playerX, playerY, gem.tint, 42)
setToast("SAPPHIRE CORE ? FROST LOCK ? " + frozen + " ENEMY/ENEMIES FROZEN", 2.2)
case 2 =>
playerHealth = math.min(100, playerHealth + 42)
charge = math.min(4, charge + 1)
burst(playerX, playerY, gem.tint, 42)
setToast("EMERALD CORE ? LIFE BLOOM ? HEALTH RESTORED", 2.2)
case 3 =>
var hits = 0
sentries.filter(s => !s.defeated && distance(playerX, playerY, s.x, s.y) < 360.0).foreach { enemy =>
sharedDamageEnemy(enemy, 76 + activeRealm * 8); hits += 1
}
burst(playerX, playerY, gem.tint, 65)
setToast("AMETHYST CORE ? NOVA BURST ? " + hits + " ENEMY/ENEMIES HIT", 2.2)
case 4 =>
sentries.filterNot(_.defeated).foreach { enemy => enemy.slowTime = 6.0 }; sendSharedAction("SLOW|6000")
burst(playerX, playerY, gem.tint, 55)
setToast("TOPAZ CORE ? TIME BEND ? ALL ENEMIES SLOWED", 2.2)
case _ => ()
}
}
private def equipGem(id: Int): Unit = {
if (id >= 0 && id < POWER_GEMS.length && collectedGems.contains(id)) {
equippedGem = id
val gem = POWER_GEMS(id)
setToast("EQUIPPED: " + gem.name + " ? PRESS R TO USE " + gem.powerName, 2.4)
refreshCollection()
} else setToast("That gem has not been collected yet.", 1.6)
}
private def showCollection(fromGame: Boolean): Unit = {
collectionFromGame = fromGame
collectionWasPaused = paused
if (fromGame) paused = true
refreshCollection()
showCard("collection")
}
private def returnFromCollection(): Unit = {
if (collectionFromGame) {
paused = collectionWasPaused
showCard("game")
if (gameCanvas != null) gameCanvas.requestFocusInWindow()
} else showCard("menu")
}
private def openAdminPanel(fromGame: Boolean): Unit = {
adminFromGame = fromGame
adminWasPaused = paused
if (fromGame) paused = true
refreshAdmin()
showCard("admin")
}
private def closeAdminPanel(): Unit = {
if (adminFromGame) {
paused = adminWasPaused
showCard("game")
if (gameCanvas != null) gameCanvas.requestFocusInWindow()
} else showCard("menu")
adminFromGame = false
}
private def sendSharedAction(action: String): Unit = {
multiplayerMode match {
case "HOST" => lanServer.foreach(_.broadcastHostAction(action))
case "JOIN" => lanClient.foreach(_.sendAction(action))
case _ => ()
}
}
private def sharedDamageEnemy(enemy: Sentry, amount: Int): Unit = {
val index = sentries.indexOf(enemy)
damageEnemy(enemy, amount)
if (index >= 0) sendSharedAction("HIT|" + index + "|" + amount)
}
private def collectGemId(id: Int): Unit = {
if (id >= 0 && id < POWER_GEMS.length && !collectedGems.contains(id)) {
collectedGems += id
droppedGemIds -= id
gemDrops.find(_.gemId == id).foreach(drop => drop.collected = true)
if (equippedGem < 0) equippedGem = id
val gem = POWER_GEMS(id)
setToast("POWER GEM COLLECTED: " + gem.name + " ? PRESS " + (id + 1) + " TO EQUIP.", 3.4)
refreshCollection()
}
}
private def applySharedAction(wire: String): Unit = {
val parts = wire.split("\\|", 4)
if (parts.length >= 2) {
parts(0) match {
case "HIT" if parts.length >= 3 =>
try {
val idx = parts(1).toInt
val amount = parts(2).toInt
if (idx >= 0 && idx < sentries.length && amount >= 1 && amount <= 250) damageEnemy(sentries(idx), amount)
} catch { case _: Throwable => () }
case "GEM" => try collectGemId(parts(1).toInt) catch { case _: Throwable => () }
case "SIGIL" =>
try { val idx = parts(1).toInt; if (idx >= 0 && idx < relics.length) relics(idx).collected = true }
catch { case _: Throwable => () }
case "FREEZE" =>
try {
val idx = parts(1).toInt
val rawDuration = if (parts.length >= 3) parts(2).toDouble / 1000.0 else 2.7
val duration = clamp(rawDuration, 0.0, 12.0)
if (idx >= 0 && idx < sentries.length && !sentries(idx).defeated)
sentries(idx).disabled = math.max(sentries(idx).disabled, duration)
} catch { case _: Throwable => () }
case "SLOW" =>
try {
val duration = clamp(parts(1).toDouble / 1000.0, 0.0, 12.0)
sentries.filterNot(_.defeated).foreach(enemy => enemy.slowTime = math.max(enemy.slowTime, duration))
} catch { case _: Throwable => () }
case _ => ()
}
}
}
private def consumeNetworkActions(): Unit = {
val actions = if (multiplayerMode == "HOST") lanServer.map(_.consumeHostActions()).getOrElse(Seq.empty)
else if (multiplayerMode == "JOIN") lanClient.map(_.consumeActions()).getOrElse(Seq.empty)
else Seq.empty
actions.foreach(applySharedAction)
}
private def applyAdminAction(command: String): Unit = {
command match {
case "HEAL" =>
playerHealth = 100
charge = math.min(4, math.max(1, charge))
failed = false
setToast("ADMIN CONTROL: HEALTH RESTORED.", 2.4)
case "GEMS" =>
POWER_GEMS.foreach(g => collectedGems += g.id)
if (equippedGem < 0) equippedGem = 0
refreshCollection()
setToast("ADMIN CONTROL: ALL FIVE POWER GEMS GRANTED.", 2.4)
case "BOOST" =>
speedBoostTime = 12.0
dashCooldown = 0.0
setToast("ADMIN CONTROL: SPEED BOOST ENABLED.", 2.4)
case "FREEZE" =>
sentries.filterNot(_.defeated).foreach(enemy => enemy.disabled = 10.0)
setToast("ADMIN CONTROL: ENEMIES FROZEN.", 2.4)
case "CLEAR" =>
sentries.foreach(s => if (!s.defeated) { s.health = 0; defeatEnemy(s) })
setToast("ADMIN CONTROL: ENEMY WAVE CLEARED.", 2.4)
case "REVIVE" =>
playerHealth = 100
charge = 4
failed = false
paused = false
invulnerability = 2.5
started = true
setToast("ADMIN CONTROL: PLAYER REVIVED.", 2.4)
case _ => setToast("Unknown Admin command.", 1.5)
}
}
private def sendAdminAction(command: String): Unit = {
if (!isAdmin) return
val choice = if (adminTargetBox == null || adminTargetBox.getSelectedItem == null) "ALL" else adminTargetBox.getSelectedItem.toString
val target = if (choice == "ALL PLAYERS") "ALL" else if (choice.startsWith("HOST ?")) "HOST" else if (choice.contains(" ? ")) choice.takeWhile(_ != ' ') else "ALL"
multiplayerMode match {
case "HOST" => lanServer match {
case Some(server) => server.broadcastHostCommand(command, target)
case None => applyAdminAction(command)
}
case "JOIN" => lanClient match {
case Some(client) if client.isAdminOnServer => client.sendAdminCommand(command, target)
case _ => JOptionPane.showMessageDialog(frame, "Admin multiplayer controls require an authorized room connection.", "Admin access", JOptionPane.WARNING_MESSAGE)
}
case _ => applyAdminAction(command)
}
refreshAdminTargets()
}
private def consumeNetworkAdminCommands(): Unit = {
val commands = if (multiplayerMode == "HOST") lanServer.map(_.consumeHostCommands()).getOrElse(Seq.empty)
else if (multiplayerMode == "JOIN") lanClient.map(_.consumeCommands()).getOrElse(Seq.empty)
else Seq.empty
commands.foreach { wire =>
val parts = wire.split("\\|", 2)
if (parts.length == 2) {
val target = parts(1)
val local = target == "ALL" || target == localNetworkId
if (local) applyAdminAction(parts(0))
}
}
}
private def hostLanGame(): Unit = {
val entered = JOptionPane.showInputDialog(frame, "Room port (default 5050):", String.valueOf(NETWORK_PORT))
if (entered == null) return
try {
val port = entered.trim.toInt
require(port >= 1024 && port <= 65535, "Port must be between 1024 and 65535.")
closeNetwork()
val server = new LanServer(port, playerName)
server.start()
lanServer = Some(server)
multiplayerMode = "HOST"
startRealm(1, true)
val hostIp = try InetAddress.getLocalHost.getHostAddress catch { case _: Throwable => "Find this PC's IPv4 address in Network Settings" }
JOptionPane.showMessageDialog(frame, "LAN room is running.\nHost address: " + hostIp + "\nPort: " + port + "\nOther players must use the same Wi-Fi or hotspot.", "Room hosted", JOptionPane.INFORMATION_MESSAGE)
setToast("LAN ROOM HOSTED ? " + hostIp + ":" + port, 5.0)
} catch {
case ex: Throwable => JOptionPane.showMessageDialog(frame, "Could not host LAN room: " + ex.getMessage, "Room host error", JOptionPane.ERROR_MESSAGE)
}
}
private def joinLanGame(): Unit = {
val host = JOptionPane.showInputDialog(frame, "Host computer IP address:", "192.168.1.2")
if (host == null || host.trim.isEmpty) return
val entered = JOptionPane.showInputDialog(frame, "Room port:", String.valueOf(NETWORK_PORT))
if (entered == null) return
try {
val port = entered.trim.toInt
require(port >= 1024 && port <= 65535, "Port must be between 1024 and 65535.")
closeNetwork()
val token = if (isAdmin) ADMIN_PIN else ""
val client = new LanClient(host.trim, port, playerName, token)
lanClient = Some(client)
multiplayerMode = "JOIN"
if (isAdmin && !client.isAdminOnServer) JOptionPane.showMessageDialog(frame, "This room did not authorize the Admin account. You can play, but cannot control other players.", "Admin access", JOptionPane.INFORMATION_MESSAGE)
startRealm(1, true)
setToast("CONNECTED TO LAN ROOM ? " + host.trim + ":" + port, 4.0)
} catch {
case ex: Throwable => JOptionPane.showMessageDialog(frame, "Could not join LAN room: " + ex.getMessage, "Join error", JOptionPane.ERROR_MESSAGE)
}
}
private def closeNetwork(): Unit = {
lanClient.foreach(_.close()); lanClient = None
lanServer.foreach(_.close()); lanServer = None
multiplayerMode = "SOLO"
}
private def doPulse(): Unit = {
if (!started || paused || cleared || failed || pulseCooldown > 0.0) return
pulseCooldown = 4.2
pulseFlash = 0.55
var affected = 0
sentries.foreach { s =>
if (!s.defeated && distance(playerX, playerY, s.x, s.y) < 245.0) {
s.disabled = 2.7
sendSharedAction("FREEZE|" + sentries.indexOf(s) + "|2700")
affected += 1
burst(s.x, s.y, C_BLUE, 13)
}
}
setToast(if (affected > 0) "LUMEN PULSE RELEASED ? nearby sentries paused." else "LUMEN PULSE RELEASED.", 2.0)
}
private def interact(): Unit = {
if (!started || paused || cleared || failed) return
val nearbyGem = gemDrops.filterNot(_.collected).sortBy(d => distance(playerX, playerY, d.x, d.y)).headOption
nearbyGem match {
case Some(drop) if distance(playerX, playerY, drop.x, drop.y) < 96.0 =>
val gemId = drop.gemId
val gem = POWER_GEMS(gemId)
drop.collected = true
collectGemId(gemId)
sendSharedAction("GEM|" + gemId)
burst(drop.x, drop.y, gem.tint, 35)
case _ =>
val nearest = relics.filterNot(_.collected).sortBy(r => distance(playerX, playerY, r.x, r.y)).headOption
nearest match {
case Some(r) if distance(playerX, playerY, r.x, r.y) < 94.0 =>
r.collected = true
sendSharedAction("SIGIL|" + relics.indexOf(r))
burst(r.x, r.y, r.tint, 25)
setToast("ACQUIRED: " + r.title + " [" + relicCount + "/" + relics.length + "]", 2.2)
if (relicCount == relics.length) setToast("ALL SIGILS SECURED ? travel to the far exit arch.", 4.0)
case _ =>
if (distance(playerX, playerY, PORTAL_X, PORTAL_Y) < 135.0) {
if (relicCount == relics.length && aliveEnemyCount == 0) {
cleared = true
completedRealms += activeRealm
burst(PORTAL_X, PORTAL_Y, C_GOLD, 80)
setToast("REALM CLEARED ? " + currentRealm.name, 6.0)
} else {
val missing = relics.length - relicCount
val remainingEnemies = aliveEnemyCount
setToast("EXIT SEALED ? SIGILS LEFT: " + missing + " ? ENEMIES LEFT: " + remainingEnemies, 2.8)
}
} else setToast("Move close to a dropped gem, sigil, or exit arch, then press E.", 2.1)
}
}
}
private def burst(x: Double, y: Double, tint: Color, amount: Int): Unit = {
var i = 0
while (i < amount) {
val a = rng.nextDouble() * math.Pi * 2.0
val speed = 35.0 + rng.nextDouble() * 150.0
sparks += new Spark(x, y, math.cos(a) * speed, math.sin(a) * speed, 2.0 + rng.nextDouble() * 4.0, tint, 0.65 + rng.nextDouble() * 1.2)
i += 1
}
}
private def updateGame(dt: Double): Unit = {
frameCount += 1
if (toastTime > 0.0) toastTime = math.max(0.0, toastTime - dt)
if (dashTime > 0.0) dashTime = math.max(0.0, dashTime - dt)
if (dashCooldown > 0.0) dashCooldown = math.max(0.0, dashCooldown - dt)
if (pulseCooldown > 0.0) pulseCooldown = math.max(0.0, pulseCooldown - dt)
if (invulnerability > 0.0) invulnerability = math.max(0.0, invulnerability - dt)
if (pulseFlash > 0.0) pulseFlash = math.max(0.0, pulseFlash - dt)
if (attackCooldown > 0.0) attackCooldown = math.max(0.0, attackCooldown - dt)
if (powerCooldown > 0.0) powerCooldown = math.max(0.0, powerCooldown - dt)
if (attackFlash > 0.0) attackFlash = math.max(0.0, attackFlash - dt)
if (speedBoostTime > 0.0) speedBoostTime = math.max(0.0, speedBoostTime - dt)
sparks.foreach { p =>
p.x += p.vx * dt
p.y += p.vy * dt
p.vx *= 0.985
p.vy *= 0.985
p.life -= dt
}
sparks.filter(_.life <= 0.0).foreach(p => sparks -= p)
while (sparks.length < 95) {
sparks += new Spark(rng.nextDouble() * WORLD_W, WORLD_H - 8.0, (rng.nextDouble() - 0.5) * 10.0, -8.0 - rng.nextDouble() * 16.0, 1.5 + rng.nextDouble() * 2.4, C_MINT, 4.0)
}
sentries.foreach { s =>
if (s.disabled > 0.0) s.disabled = math.max(0.0, s.disabled - dt)
if (s.slowTime > 0.0) s.slowTime = math.max(0.0, s.slowTime - dt)
if (s.hitFlash > 0.0) s.hitFlash = math.max(0.0, s.hitFlash - dt)
val slowFactor = if (s.slowTime > 0.0) 0.20 else 1.0
val t = frameCount * 0.021 * s.pace * slowFactor + s.phase
// A frozen enemy really stops moving; a slowed enemy moves at 20% pace.
if (!s.defeated && s.disabled <= 0.0) {
s.x = s.baseX + math.sin(t) * s.radius
s.y = s.baseY + math.cos(t * 0.72) * 34.0
}
}
consumeNetworkAdminCommands()
consumeNetworkActions()
if (multiplayerMode == "HOST" && frameCount % 6 == 0L) {
lanServer.foreach(_.publishHostPosition(playerX, playerY, playerHealth, activeRealm, selectedDifficulty, selectedMode))
} else if (multiplayerMode == "JOIN" && frameCount % 6 == 0L) {
lanClient.foreach(_.sendPosition(playerX, playerY, playerHealth, activeRealm))
lanClient.foreach { client =>
val config = client.worldConfig
if (config._1 != activeRealm || config._2 != selectedDifficulty || config._3 != selectedMode) {
activeRealm = config._1
selectedDifficulty = config._2
selectedMode = config._3
buildRealm()
setToast("ROOM SETTINGS UPDATED ? REALM " + activeRealm + ".", 3.0)
}
}
}
if (!started || paused || cleared || failed) return
if (selectedMode != "FREE ROAM") {
secondsLeft = math.max(0.0, secondsLeft - dt)
if (secondsLeft <= 0.0) {
failed = true
setToast("TIME WINDOW CLOSED ? press T to retry this realm.", 6.0)
return
}
}
var mx = 0.0
var my = 0.0
if (keys.contains(KeyEvent.VK_W) || keys.contains(KeyEvent.VK_UP)) my -= 1.0
if (keys.contains(KeyEvent.VK_S) || keys.contains(KeyEvent.VK_DOWN)) my += 1.0
if (keys.contains(KeyEvent.VK_A) || keys.contains(KeyEvent.VK_LEFT)) mx -= 1.0
if (keys.contains(KeyEvent.VK_D) || keys.contains(KeyEvent.VK_RIGHT)) mx += 1.0
val length = math.sqrt(mx * mx + my * my)
if (length > 0.0) {
mx /= length
my /= length
facingX = mx
facingY = my
val speed = if (dashTime > 0.0) 465.0 else if (speedBoostTime > 0.0) 315.0 else 225.0
playerX = clamp(playerX + mx * speed * dt, 35.0, WORLD_W - 35.0)
playerY = clamp(playerY + my * speed * dt, 35.0, WORLD_H - 35.0)
}
sentries.foreach { enemy =>
if (!enemy.defeated && enemy.disabled <= 0.0 && invulnerability <= 0.0 && distance(playerX, playerY, enemy.x, enemy.y) < 45.0) {
playerHealth -= 18 + activeRealm * 2
invulnerability = 0.90
playerX = clamp(playerX - facingX * 54.0, 35.0, WORLD_W - 35.0)
playerY = clamp(playerY - facingY * 54.0, 35.0, WORLD_H - 35.0)
burst(playerX, playerY, C_PINK, 13)
setToast("ENEMY HIT ? HEALTH " + math.max(0, playerHealth) + "/100", 1.7)
if (playerHealth <= 0) {
charge -= 1
if (charge > 0) { playerHealth = 62; invulnerability = 2.0; setToast("CHARGE USED ? " + charge + " CHARGE(S) LEFT.", 2.4) }
else { failed = true; setToast("NO CHARGES LEFT ? press T to retry this realm.", 5.0) }
}
}
}
}
// ---------------------------------------------------------------------------
// REAL-TIME PERSPECTIVE 3D RENDERER (software-rendered polygons; no libraries)
// ---------------------------------------------------------------------------
private final case class V3(x: Double, y: Double, z: Double)
private final case class P3(x: Int, y: Int, depth: Double)
private final case class Face3(points: Vector[V3], fill: Color, edge: Color)
private var cameraDistanceIndex = 1
private val CAMERA_DISTANCES: Vector[Double] = Vector(340.0, 470.0, 630.0)
private val CAMERA_LABELS: Vector[String] = Vector("CLOSE", "FOLLOW", "WIDE")
private def cameraForward: (Double, Double) = {
val n = math.sqrt(facingX * facingX + facingY * facingY)
if (n < 0.0001) (0.0, -1.0) else (facingX / n, facingY / n)
}
/** Perspective projection from world X/Y plus vertical Z into a third-person camera. */
private def project3D(v: V3, w: Int, h: Int): Option[P3] = {
val f = cameraForward
val fx = f._1
val fy = f._2
val rx = -fy
val ry = fx
val camDistance = CAMERA_DISTANCES(cameraDistanceIndex)
val camX = playerX - fx * camDistance
val camY = playerY - fy * camDistance
val camZ = camDistance * 0.53
val dx = v.x - camX
val dy = v.y - camY
val dz = v.z - camZ
val along = dx * fx + dy * fy
val side = dx * rx + dy * ry
val sinTilt = 0.34
val cosTilt = 0.941
val depth = along * cosTilt - dz * sinTilt
if (depth <= 34.0 || depth > 5200.0) None
else {
val vertical = along * sinTilt + dz * cosTilt
val focal = math.min(w.toDouble, h.toDouble) * 0.92
val sx = w * 0.5 + side * focal / depth
val sy = h * 0.49 - vertical * focal / depth
if (sx < -w * 2.5 || sx > w * 3.5 || sy < -h * 3.0 || sy > h * 3.5) None
else Some(P3(sx.toInt, sy.toInt, depth))
}
}
private def shade3D(c: Color, factor: Double): Color = {
val r = math.max(0, math.min(255, (c.getRed * factor).toInt))
val g = math.max(0, math.min(255, (c.getGreen * factor).toInt))
val b = math.max(0, math.min(255, (c.getBlue * factor).toInt))
new Color(r, g, b, c.getAlpha)
}
private def addBox3D(out: ArrayBuffer[Face3], x: Double, y: Double, z: Double,
width: Double, depth: Double, height: Double, base: Color): Unit = {
val hx = width * 0.5
val hy = depth * 0.5
val a = V3(x - hx, y - hy, z)
val b = V3(x + hx, y - hy, z)
val c = V3(x + hx, y + hy, z)
val d = V3(x - hx, y + hy, z)
val A = V3(x - hx, y - hy, z + height)
val B = V3(x + hx, y - hy, z + height)
val C = V3(x + hx, y + hy, z + height)
val D = V3(x - hx, y + hy, z + height)
val edge = shade3D(base, 0.48)
out += Face3(Vector(A, B, C, D), shade3D(base, 1.34), edge)
out += Face3(Vector(a, b, B, A), shade3D(base, 0.88), edge)
out += Face3(Vector(b, c, C, B), shade3D(base, 0.70), edge)
out += Face3(Vector(c, d, D, C), shade3D(base, 0.82), edge)
out += Face3(Vector(d, a, A, D), shade3D(base, 1.02), edge)
out += Face3(Vector(d, c, b, a), shade3D(base, 0.48), edge)
}
private def addDisc3D(out: ArrayBuffer[Face3], x: Double, y: Double, z: Double,
rx: Double, ry: Double, tint: Color): Unit = {
var i = 0
val segments = 14
while (i < segments) {
val a0 = i * math.Pi * 2.0 / segments
val a1 = (i + 1) * math.Pi * 2.0 / segments
val center = V3(x, y, z)
val p0 = V3(x + math.cos(a0) * rx, y + math.sin(a0) * ry, z)
val p1 = V3(x + math.cos(a1) * rx, y + math.sin(a1) * ry, z)
out += Face3(Vector(center, p0, p1), tint, new Color(12, 24, 40, 0))
i += 1
}
}
private def fog3D(c: Color, depth: Double): Color = {
val amount = clamp((depth - 760.0) / 3000.0, 0.0, 0.48)
val fog = new Color(38, 76, 103)
val r = (c.getRed * (1.0 - amount) + fog.getRed * amount).toInt
val g = (c.getGreen * (1.0 - amount) + fog.getGreen * amount).toInt
val b = (c.getBlue * (1.0 - amount) + fog.getBlue * amount).toInt
new Color(math.max(0, math.min(255, r)), math.max(0, math.min(255, g)), math.max(0, math.min(255, b)), c.getAlpha)
}
private def addOcta3D(out: ArrayBuffer[Face3], x: Double, y: Double, z: Double,
radius: Double, height: Double, tint: Color): Unit = {
val top = V3(x, y, z + height)
val bottom = V3(x, y, z - height * 0.72)
val left = V3(x - radius, y, z)
val right = V3(x + radius, y, z)
val near = V3(x, y - radius, z)
val far = V3(x, y + radius, z)
val edge = shade3D(tint, 0.43)
out += Face3(Vector(top, near, right), shade3D(tint, 1.28), edge)
out += Face3(Vector(top, right, far), shade3D(tint, 0.94), edge)
out += Face3(Vector(top, far, left), shade3D(tint, 1.12), edge)
out += Face3(Vector(top, left, near), shade3D(tint, 0.82), edge)
out += Face3(Vector(bottom, right, near), shade3D(tint, 0.62), edge)
out += Face3(Vector(bottom, far, right), shade3D(tint, 0.76), edge)
out += Face3(Vector(bottom, left, far), shade3D(tint, 0.68), edge)
out += Face3(Vector(bottom, near, left), shade3D(tint, 0.55), edge)
}
/** A low-poly UV sphere: intentionally faceted for a lightweight game renderer. */
private def addSphere3D(out: ArrayBuffer[Face3], x: Double, y: Double, z: Double,
radius: Double, tint: Color): Unit = {
val bands = 4
val slices = 7
var lat = 0
while (lat < bands) {
val p0 = -math.Pi / 2.0 + math.Pi * lat / bands
val p1 = -math.Pi / 2.0 + math.Pi * (lat + 1) / bands
var lon = 0
while (lon < slices) {
val t0 = math.Pi * 2.0 * lon / slices + frameCount * 0.0008
val t1 = math.Pi * 2.0 * (lon + 1) / slices + frameCount * 0.0008
def point(phi: Double, theta: Double): V3 = V3(
x + math.cos(phi) * math.cos(theta) * radius,
y + math.cos(phi) * math.sin(theta) * radius,
z + math.sin(phi) * radius
)
val factor = 0.78 + 0.25 * math.max(0.0, math.sin((lon.toDouble / slices) * math.Pi * 2.0 + 0.8))
out += Face3(Vector(point(p0, t0), point(p0, t1), point(p1, t1), point(p1, t0)),
shade3D(tint, factor), shade3D(tint, 0.42))
lon += 1
}
lat += 1
}
}
private def render3DFaces(g: Graphics2D, faces: ArrayBuffer[Face3], w: Int, h: Int): Unit = {
val projected = ArrayBuffer[(Double, Face3, Vector[P3])]()
faces.foreach { face =>
val points = face.points.map(v => project3D(v, w, h))
if (points.forall(_.isDefined)) {
val valid = points.map(_.get)
val averageDepth = valid.map(_.depth).sum / math.max(1, valid.length)
projected += ((averageDepth, face, valid))
}
}
// Painter's algorithm: far polygons first, near polygons last.
projected.sortBy(item => -item._1).foreach { item =>
val face = item._2
val pts = item._3
val xs = pts.map(_.x).toArray
val ys = pts.map(_.y).toArray
g.setColor(fog3D(face.fill, item._1))
g.fillPolygon(xs, ys, pts.length)
g.setColor(fog3D(face.edge, item._1))
g.setStroke(new BasicStroke(1.0f))
g.drawPolygon(xs, ys, pts.length)
}
}
private def draw3DLine(g: Graphics2D, a: V3, b: V3, color: Color, width: Float, w: Int, h: Int): Unit = {
val pa = project3D(a, w, h)
val pb = project3D(b, w, h)
if (pa.isDefined && pb.isDefined) {
g.setColor(color)
g.setStroke(new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))
g.drawLine(pa.get.x, pa.get.y, pb.get.x, pb.get.y)
}
}
private def draw3DHorizontalRing(g: Graphics2D, x: Double, y: Double, z: Double,
radius: Double, tint: Color, w: Int, h: Int): Unit = {
var i = 0
while (i < 44) {
val a0 = i * math.Pi * 2.0 / 44.0
val a1 = (i + 1) * math.Pi * 2.0 / 44.0
draw3DLine(g, V3(x + math.cos(a0) * radius, y + math.sin(a0) * radius, z),
V3(x + math.cos(a1) * radius, y + math.sin(a1) * radius, z), tint, 2.0f, w, h)
i += 1
}
}
private def draw3DVerticalRing(g: Graphics2D, x: Double, y: Double, z: Double,
radius: Double, tint: Color, w: Int, h: Int): Unit = {
val f = cameraForward
val rx = -f._2
val ry = f._1
var i = 0
while (i < 52) {
val a0 = i * math.Pi * 2.0 / 52.0
val a1 = (i + 1) * math.Pi * 2.0 / 52.0
val p0 = V3(x + rx * math.cos(a0) * radius, y + ry * math.cos(a0) * radius, z + math.sin(a0) * radius)
val p1 = V3(x + rx * math.cos(a1) * radius, y + ry * math.cos(a1) * radius, z + math.sin(a1) * radius)
draw3DLine(g, p0, p1, tint, 4.0f, w, h)
i += 1
}
}
private def draw3DLabel(g: Graphics2D, text: String, x: Double, y: Double, z: Double,
color: Color, size: Int, w: Int, h: Int): Unit = {
project3D(V3(x, y, z), w, h).foreach { p =>
g.setFont(new Font("Dialog", Font.BOLD, size))
g.setColor(new Color(4, 12, 28, 190))
drawCentered(g, text, p.x + 1, p.y + 2)
g.setColor(color)
drawCentered(g, text, p.x, p.y)
}
}
private def draw3DBackdrop(g: Graphics2D, w: Int, h: Int): Unit = {
val upper = colorFromHue(currentRealm.hue, 0.76f, 0.25f)
val lower = new Color(8, 23, 43)
g.setPaint(new GradientPaint(0f, 0f, upper, 0f, h.toFloat, lower))
g.fillRect(0, 0, w, h)
stars.foreach { star =>
val x = ((star.x / WORLD_W) * w).toInt
val y = ((star.y / WORLD_H) * (h * 0.43)).toInt
val alpha = 90 + math.abs((math.sin(frameCount * 0.055 + star.phase) * 145).toInt)
g.setColor(new Color(226, 244, 255, alpha))
g.fillOval(x, y, star.size + 1, star.size + 1)
}
val mx = (w * 0.78).toInt
val my = (h * 0.16).toInt
drawGlow(g, mx, my, 118, C_GOLD, 0.13f)
g.setColor(new Color(255, 230, 185))
g.fillOval(mx - 31, my - 31, 62, 62)
g.setColor(new Color(255, 255, 255, 110))
g.drawOval(mx - 44, my - 44, 88, 88)
}
private def draw3DScene(g: Graphics2D, w: Int, h: Int): Unit = {
draw3DBackdrop(g, w, h)
val groundFaces = ArrayBuffer[Face3]()
// Floating world foundation plus individually projected floor tiles. Small tiles
// keep the camera's near-plane clipping from removing the entire ground polygon.
addBox3D(groundFaces, WORLD_W * 0.5, WORLD_H * 0.5, -42.0, WORLD_W, WORLD_H, 42.0, new Color(21, 91, 88))
var tileX = 0.0
while (tileX < WORLD_W) {
var tileY = 0.0
while (tileY < WORLD_H) {
val tileTint = if (((tileX / 240.0).toInt + (tileY / 220.0).toInt) % 2 == 0) new Color(23, 91, 82) else new Color(18, 78, 80)
groundFaces += Face3(Vector(V3(tileX, tileY, 0.0), V3(tileX + 240.0, tileY, 0.0),
V3(tileX + 240.0, tileY + 220.0, 0.0), V3(tileX, tileY + 220.0, 0.0)),
tileTint, new Color(84, 177, 154, 80))
tileY += 220.0
}
tileX += 240.0
}
// Soft low-poly contact shadows make the raised geometry feel grounded.
var shadowIndex = 0
while (shadowIndex < 12) {
val sx = 145.0 + (shadowIndex % 4) * 510.0
val sy = 170.0 + (shadowIndex / 4) * 420.0 + (shadowIndex % 2) * 65.0
addDisc3D(groundFaces, sx, sy, 1.8, 112.0, 74.0, new Color(3, 13, 25, 55))
shadowIndex += 1
}
render3DFaces(g, groundFaces, w, h)
// Perspective floor grid, drawn above the ground tiles but below all props.
var gx = 0.0
while (gx <= WORLD_W) {
draw3DLine(g, V3(gx, 0.0, 1.2), V3(gx, WORLD_H, 1.2), new Color(98, 223, 194, 72), 1.0f, w, h)
gx += 150.0
}
var gy = 0.0
while (gy <= WORLD_H) {
draw3DLine(g, V3(0.0, gy, 1.2), V3(WORLD_W, gy, 1.2), new Color(98, 223, 194, 72), 1.0f, w, h)
gy += 150.0
}
val faces = ArrayBuffer[Face3]()
// Raised stone platforms and layered walkways.
var platform = 0
while (platform < 12) {
val px = 145.0 + (platform % 4) * 510.0
val py = 170.0 + (platform / 4) * 420.0 + (platform % 2) * 65.0
addBox3D(faces, px, py, 0.0, 190.0, 126.0, 38.0, new Color(35, 113, 104))
addBox3D(faces, px, py, 38.0, 154.0, 94.0, 8.0, new Color(77, 178, 137))
platform += 1
}
// Sanctus engine: layered plinths, eight pillars, animated rings and a crystal core.
addBox3D(faces, 960.0, 600.0, 0.0, 300.0, 245.0, 38.0, new Color(36, 69, 105))
addBox3D(faces, 960.0, 600.0, 38.0, 240.0, 185.0, 24.0, new Color(57, 115, 138))
var pillar = 0
while (pillar < 8) {
val angle = pillar * math.Pi * 2.0 / 8.0
val px = 960.0 + math.cos(angle) * 116.0
val py = 600.0 + math.sin(angle) * 91.0
addBox3D(faces, px, py, 62.0, 23.0, 23.0, 152.0, new Color(62, 109, 143))
addBox3D(faces, px, py, 214.0, 33.0, 33.0, 15.0, new Color(139, 215, 194))
addSphere3D(faces, px, py, 239.0, 10.0, if (pillar % 2 == 0) C_GOLD else C_MINT)
pillar += 1
}
addSphere3D(faces, 960.0, 600.0, 154.0, 48.0, colorFromHue(currentRealm.hue, 0.58f, 1.0f))
addOcta3D(faces, 960.0, 600.0, 185.0 + math.sin(frameCount * 0.055) * 8.0, 33.0, 76.0, C_GOLD)
// Low-poly trees and glowing growths, spaced through the whole realm.
var bloomIndex = 0
blooms.foreach { bloom =>
if (bloomIndex % 18 == 0) {
addBox3D(faces, bloom.x, bloom.y, 0.0, 17.0, 17.0, 62.0, new Color(87, 64, 53))
addSphere3D(faces, bloom.x, bloom.y, 93.0, 39.0, colorFromHue(bloom.hue, 0.64f, 0.76f))
addSphere3D(faces, bloom.x - 19.0, bloom.y + 7.0, 111.0, 22.0, colorFromHue(bloom.hue + 14, 0.56f, 0.92f))
}
bloomIndex += 1
}
// Collectible sigils and floating power cores.
relics.foreach { relic =>
if (!relic.collected) {
val bob = math.sin(frameCount * 0.075 + relic.x * 0.01) * 9.0
addBox3D(faces, relic.x, relic.y, 0.0, 40.0, 40.0, 10.0, new Color(40, 89, 102))
addOcta3D(faces, relic.x, relic.y, 42.0 + bob, 17.0, 31.0, relic.tint)
}
}
gemDrops.foreach { drop =>
if (!drop.collected) {
val gem = POWER_GEMS(drop.gemId)
val bob = math.sin(frameCount * 0.10 + drop.gemId) * 12.0
addOcta3D(faces, drop.x, drop.y, 52.0 + bob, 22.0, 39.0, gem.tint)
addSphere3D(faces, drop.x, drop.y, 53.0 + bob, 10.0, Color.WHITE)
}
}
// Sentries are fully built from 3D body, head, armour, shoulders and feet meshes.
sentries.foreach { enemy =>
if (!enemy.defeated) {
addDisc3D(faces, enemy.x, enemy.y, 2.0, 43.0, 34.0, new Color(2, 9, 19, 105))
val body = if (enemy.disabled > 0.0) C_BLUE else if (enemy.hitFlash > 0.0) C_WHITE else enemy.tint
val scale = 1.0 + enemy.rank * 0.055
addBox3D(faces, enemy.x - 15.0, enemy.y, 0.0, 25.0 * scale, 31.0 * scale, 38.0 * scale, new Color(33, 37, 63))
addBox3D(faces, enemy.x + 15.0, enemy.y, 0.0, 25.0 * scale, 31.0 * scale, 38.0 * scale, new Color(33, 37, 63))
addBox3D(faces, enemy.x, enemy.y, 34.0 * scale, 78.0 * scale, 54.0 * scale, 72.0 * scale, shade3D(body, 0.79))
addBox3D(faces, enemy.x, enemy.y - 3.0, 49.0 * scale, 54.0 * scale, 59.0 * scale, 54.0 * scale, body)
addSphere3D(faces, enemy.x, enemy.y - 5.0, 116.0 * scale, 31.0 * scale, shade3D(body, 1.12))
addOcta3D(faces, enemy.x - 23.0 * scale, enemy.y - 4.0, 144.0 * scale, 10.0 * scale, 22.0 * scale, C_PINK)
addOcta3D(faces, enemy.x + 23.0 * scale, enemy.y - 4.0, 144.0 * scale, 10.0 * scale, 22.0 * scale, C_PINK)
addSphere3D(faces, enemy.x - 11.0 * scale, enemy.y - 31.0, 120.0 * scale, 6.0 * scale, C_GOLD)
addSphere3D(faces, enemy.x + 11.0 * scale, enemy.y - 31.0, 120.0 * scale, 6.0 * scale, C_GOLD)
}
}
// Local player: boots, legs, torso, shoulders, head and the five-slot gauntlet.
val playerBlink = invulnerability > 0.0 && (frameCount / 5) % 2 == 0
if (!playerBlink) {
addDisc3D(faces, playerX, playerY, 2.2, 34.0, 29.0, new Color(2, 9, 19, 125))
val bob = math.sin(frameCount * 0.18) * 3.0
addBox3D(faces, playerX - 15.0, playerY, 0.0, 22.0, 27.0, 48.0 + bob, new Color(31, 48, 82))
addBox3D(faces, playerX + 15.0, playerY, 0.0, 22.0, 27.0, 48.0 - bob, new Color(31, 48, 82))
addBox3D(faces, playerX, playerY, 45.0, 57.0, 38.0, 73.0 + bob, new Color(36, 91, 134))
addBox3D(faces, playerX, playerY - 2.0, 58.0, 43.0, 43.0, 49.0 + bob, new Color(90, 211, 190))
addSphere3D(faces, playerX, playerY, 143.0 + bob, 24.0, new Color(238, 202, 171))
addSphere3D(faces, playerX, playerY + 2.0, 158.0 + bob, 25.0, new Color(42, 32, 59))
addBox3D(faces, playerX - 39.0, playerY, 57.0, 19.0, 25.0, 55.0, new Color(43, 105, 143))
addBox3D(faces, playerX + 39.0, playerY, 57.0, 19.0, 25.0, 55.0, new Color(43, 105, 143))
val f = cameraForward
val gloveX = playerX - f._2 * 43.0 + f._1 * 10.0
val gloveY = playerY + f._1 * 43.0 + f._2 * 10.0
addBox3D(faces, gloveX, gloveY, 71.0, 27.0, 29.0, 30.0, new Color(167, 122, 60))
currentGem.foreach { gem =>
addOcta3D(faces, gloveX, gloveY - 2.0, 104.0, 12.0, 19.0, gem.tint)
}
}
// Remote players are also rendered as 3D avatars in the shared arena.
networkPlayers.foreach { peer =>
if (peer.id != localNetworkId) {
addBox3D(faces, peer.x - 13.0, peer.y, 0.0, 22.0, 25.0, 43.0, new Color(30, 50, 95))
addBox3D(faces, peer.x + 13.0, peer.y, 0.0, 22.0, 25.0, 43.0, new Color(30, 50, 95))
addBox3D(faces, peer.x, peer.y, 42.0, 52.0, 37.0, 69.0, new Color(45, 117, 171))
addSphere3D(faces, peer.x, peer.y, 132.0, 23.0, new Color(225, 194, 166))
}
}
// Exit gate and animated energy effects.
addBox3D(faces, PORTAL_X, PORTAL_Y + 42.0, 0.0, 155.0, 65.0, 22.0, new Color(47, 71, 114))
render3DFaces(g, faces, w, h)
draw3DVerticalRing(g, 960.0, 600.0, 138.0, 116.0, C_MINT, w, h)
draw3DHorizontalRing(g, 960.0, 600.0, 4.0, 160.0, new Color(100, 229, 218, 190), w, h)
draw3DVerticalRing(g, PORTAL_X, PORTAL_Y, 102.0, 86.0, if (relicCount == relics.length && aliveEnemyCount == 0) C_GOLD else C_VIOLET, w, h)
draw3DHorizontalRing(g, playerX, playerY, 3.0, 34.0, new Color(170, 255, 220, 135), w, h)
relics.foreach { relic => if (!relic.collected) draw3DLabel(g, relic.title, relic.x, relic.y, 86.0, relic.tint, 9, w, h) }
gemDrops.foreach { drop => if (!drop.collected) draw3DLabel(g, POWER_GEMS(drop.gemId).name, drop.x, drop.y, 105.0, POWER_GEMS(drop.gemId).tint, 10, w, h) }
sentries.foreach { enemy =>
if (!enemy.defeated) {
draw3DLabel(g, if (enemy.disabled > 0.0) "FROZEN" else "SENTRY " + enemy.rank,
enemy.x, enemy.y, 177.0, C_WHITE, 10, w, h)
val barLeft = project3D(V3(enemy.x - 32.0, enemy.y, 166.0), w, h)
val barRight = project3D(V3(enemy.x + 32.0, enemy.y, 166.0), w, h)
if (barLeft.isDefined && barRight.isDefined) {
val left = barLeft.get
val right = barRight.get
val barW = math.max(1, right.x - left.x)
g.setColor(new Color(23, 31, 49, 220)); g.fillRoundRect(left.x, left.y, barW, 6, 4, 4)
g.setColor(C_PINK); g.fillRoundRect(left.x, left.y, (barW * enemy.health.toDouble / enemy.maxHealth).toInt, 6, 4, 4)
}
}
}
draw3DLabel(g, playerName, playerX, playerY, 194.0, C_GOLD, 11, w, h)
draw3DLabel(g, "SANCTUS ENGINE", 960.0, 600.0, 285.0, C_MINT, 12, w, h)
draw3DLabel(g, if (relicCount == relics.length && aliveEnemyCount == 0) "GATE OPEN" else "GATE SEALED",
PORTAL_X, PORTAL_Y, 206.0, C_GOLD, 11, w, h)
// Sparks float above the floor in world space.
sparks.foreach { spark =>
val height = 10.0 + (1.0 - clamp(spark.life / 2.0, 0.0, 1.0)) * 38.0
project3D(V3(spark.x, spark.y, height), w, h).foreach { p =>
val alpha = clamp(spark.life / 3.0, 0.0, 0.82).toFloat
val old = g.getComposite
g.setComposite(AlphaComposite.SrcOver.derive(alpha))
drawGlow(g, p.x, p.y, math.max(3, spark.size.toInt + 2), spark.tint, 0.24f)
g.setColor(spark.tint); g.fillOval(p.x - 2, p.y - 2, 4, 4)
g.setComposite(old)
}
}
if (attackFlash > 0.0) {
val f = cameraForward
draw3DVerticalRing(g, playerX + f._1 * 75.0, playerY + f._2 * 75.0, 72.0,
45.0 + (1.0 - attackFlash / 0.20) * 38.0, C_GOLD, w, h)
}
if (pulseFlash > 0.0) {
draw3DHorizontalRing(g, playerX, playerY, 8.0,
75.0 + (0.55 - pulseFlash) * 260.0, new Color(90, 202, 255, 190), w, h)
}
g.setFont(new Font("Dialog", Font.BOLD, 10))
g.setColor(new Color(240, 250, 255, 210))
g.drawString("REAL-TIME PERSPECTIVE VIEW ? CAMERA: " + CAMERA_LABELS(cameraDistanceIndex) + " ? V: CAMERA DISTANCE", 18, h - 62)
}
// ---------------------------------------------------------------------------
// DRAWING UTILITIES
// ---------------------------------------------------------------------------
private def drawGlow(g: Graphics2D, x: Int, y: Int, radius: Int, color: Color, alpha: Float): Unit = {
val old = g.getComposite
g.setComposite(AlphaComposite.SrcOver.derive(alpha))
g.setColor(color)
g.fillOval(x - radius, y - radius, radius * 2, radius * 2)
g.setComposite(old)
}
private def drawCentered(g: Graphics2D, text: String, x: Int, y: Int): Unit = {
g.drawString(text, x - g.getFontMetrics.stringWidth(text) / 2, y)
}
private def drawBackdrop(g: Graphics2D, w: Int, h: Int): Unit = {
val hue = currentRealm.hue
val top = colorFromHue(hue, 0.73f, 0.22f)
val bottom = new Color(8, 34, 49)
g.setPaint(new GradientPaint(0f, 0f, top, 0f, h.toFloat, bottom))
g.fillRect(0, 0, w, h)
stars.foreach { s =>
val x = worldX(s.x)
val y = worldY(s.y)
if (x >= 0 && y >= 0 && x < w && y < h) {
val alpha = 95 + math.abs((math.sin(frameCount * 0.045 + s.phase) * 145).toInt)
g.setColor(new Color(230, 247, 255, alpha))
g.fillOval(x, y, s.size, s.size)
}
}
val moonX = worldX(1470.0)
val moonY = worldY(155.0)
drawGlow(g, moonX, moonY, 115, C_GOLD, 0.12f)
g.setColor(new Color(255, 229, 177))
g.fillOval(moonX - 34, moonY - 34, 68, 68)
g.setColor(new Color(255, 255, 255, 110))
g.drawOval(moonX - 45, moonY - 45, 90, 90)
}
private def drawTerrain(g: Graphics2D, w: Int, h: Int): Unit = {
val top = (h * 0.31).toInt
g.setPaint(new GradientPaint(0f, top.toFloat, new Color(30, 104, 93), 0f, h.toFloat, new Color(5, 34, 46)))
g.fillRect(0, top, w, h - top)
val old = g.getComposite
g.setComposite(AlphaComposite.SrcOver.derive(0.075f))
g.setColor(C_MINT)
var x = 0
while (x < w) { g.drawLine(x, top, x, h); x += 78 }
var y = top
while (y < h) { g.drawLine(0, y, w, y); y += 78 }
g.setComposite(old)
// Floating garden shelves add depth without external images.
var i = 0
while (i < 12) {
val wx = 130.0 + i * 157.0
val wy = 215.0 + (i % 3) * 295.0
val sx = worldX(wx)
val sy = worldY(wy)
if (sx > -170 && sx < w + 170 && sy > -100 && sy < h + 100) {
g.setColor(new Color(6, 36, 47, 170))
g.fillOval(sx - 65, sy - 8, 130, 42)
g.setColor(new Color(71, 170, 134, 150))
g.fillOval(sx - 62, sy - 15, 124, 29)
g.setColor(new Color(168, 246, 213, 110))
g.drawArc(sx - 42, sy - 17, 84, 20, 15, 145)
}
i += 1
}
}
private def drawLandmark(g: Graphics2D): Unit = {
val cx = worldX(960.0)
val cy = worldY(600.0)
val tone = colorFromHue(currentRealm.hue, 0.7f, 0.95f)
drawGlow(g, cx, cy - 55, 190, tone, 0.10f)
// New central landmark: a multi-ring lumen engine.
g.setColor(new Color(13, 25, 49, 215))
g.fillOval(cx - 120, cy - 93, 240, 186)
g.setStroke(new BasicStroke(5.0f))
g.setColor(tone)
g.drawOval(cx - 105, cy - 115, 210, 210)
g.setStroke(new BasicStroke(2.0f))
g.setColor(new Color(232, 252, 255, 190))
g.drawOval(cx - 82, cy - 91, 164, 164)
var i = 0
while (i < 9) {
val a = frameCount * 0.012 + i * math.Pi * 2.0 / 9.0
val px = cx + (math.cos(a) * 92).toInt
val py = cy - 10 + (math.sin(a) * 92).toInt
g.setColor(if (i % 2 == 0) C_GOLD else C_MINT)
g.fillOval(px - 7, py - 7, 14, 14)
i += 1
}
g.setColor(new Color(35, 77, 97))
g.fillRoundRect(cx - 22, cy - 145, 44, 100, 15, 15)
g.setColor(new Color(190, 240, 233))
g.fillPolygon(Array(cx, cx + 18, cx, cx - 18), Array(cy - 182, cy - 139, cy - 95, cy - 139), 4)
g.setFont(new Font("Dialog", Font.BOLD, 12))
g.setColor(C_WHITE)
drawCentered(g, "LUMEN ENGINE", cx, cy + 132)
}
private def drawBlooms(g: Graphics2D): Unit = {
blooms.foreach { b =>
val x = worldX(b.x)
val y = worldY(b.y)
if (x > -20 && y > -20 && x < 1940 && y < 1340) {
val r = math.max(2, (b.size * (0.86 + 0.14 * math.sin(frameCount * 0.05 + b.phase))).toInt)
g.setColor(new Color(40, 128, 88))
g.drawLine(x, y + 2, x, y + r * 3)
g.setColor(colorFromHue(b.hue))
var k = 0
while (k < 5) {
val a = k * math.Pi * 2.0 / 5.0
g.fillOval(x + (math.cos(a) * r * 0.72).toInt - r / 2, y + (math.sin(a) * r * 0.72).toInt - r / 2, r, r)
k += 1
}
g.setColor(C_GOLD)
g.fillOval(x - 2, y - 2, 4, 4)
}
}
}
private def drawRelic(g: Graphics2D, r: Relic): Unit = {
if (r.collected) return
val x = worldX(r.x)
val y = worldY(r.y)
if (x < -75 || y < -75 || x > 1975 || y > 1395) return
val pulse = 13 + (3 * math.sin(frameCount * 0.075 + r.x)).toInt
drawGlow(g, x, y, pulse * 3, r.tint, 0.13f)
g.setColor(r.tint)
g.fillPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
g.setColor(C_WHITE)
g.drawPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
g.setFont(new Font("Dialog", Font.BOLD, 9))
drawCentered(g, r.title, x, y - pulse - 14)
g.setColor(C_MINT)
drawCentered(g, "E: TAKE", x, y + pulse + 17)
}
private def drawSentry(g: Graphics2D, s: Sentry): Unit = {
if (s.defeated) return
val x = worldX(s.x)
val y = worldY(s.y)
if (x < -90 || y < -90 || x > 1980 || y > 1400) return
val body = if (s.disabled > 0.0) C_BLUE else if (s.hitFlash > 0.0) C_WHITE else s.tint
drawGlow(g, x, y, 52, body, if (s.disabled > 0.0) 0.10f else 0.15f)
g.setColor(new Color(23, 30, 59))
g.fillOval(x - 30, y - 27, 60, 54)
g.setColor(body)
g.fillOval(x - 25, y - 24, 50, 47)
g.setColor(new Color(255, 250, 228))
g.fillOval(x - 12, y - 6, 8, 9)
g.fillOval(x + 5, y - 6, 8, 9)
g.setColor(new Color(37, 42, 63))
g.fillOval(x - 10, y - 3, 3, 4)
g.fillOval(x + 8, y - 3, 3, 4)
g.setColor(new Color(38, 48, 69))
g.fillRoundRect(x - 27, y - 42, 54, 6, 4, 4)
g.setColor(C_PINK)
g.fillRoundRect(x - 27, y - 42, (54.0 * s.health / math.max(1, s.maxHealth)).toInt, 6, 4, 4)
g.setFont(new Font("Dialog", Font.BOLD, 9))
g.setColor(C_WHITE)
drawCentered(g, if (s.disabled > 0.0) "FROZEN" else "ENEMY T" + s.rank, x, y - 50)
}
private def drawGemDrop(g: Graphics2D, drop: GemDrop): Unit = {
if (drop.collected) return
val gem = POWER_GEMS(drop.gemId)
val x = worldX(drop.x)
val y = worldY(drop.y)
val pulse = 12 + (3 * math.sin(frameCount * 0.09 + drop.gemId)).toInt
drawGlow(g, x, y, pulse * 3, gem.tint, 0.18f)
g.setColor(gem.tint)
g.fillPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
g.setColor(C_WHITE)
g.drawPolygon(Array(x, x + pulse, x, x - pulse), Array(y - pulse, y, y + pulse, y), 4)
g.setFont(new Font("Dialog", Font.BOLD, 9))
drawCentered(g, gem.name, x, y - pulse - 15)
g.setColor(C_GOLD)
drawCentered(g, "E: COLLECT", x, y + pulse + 17)
}
private def drawExitArch(g: Graphics2D): Unit = {
val x = worldX(PORTAL_X)
val y = worldY(PORTAL_Y)
val ready = relicCount == relics.length && aliveEnemyCount == 0
val tint = if (ready) C_GOLD else C_VIOLET
val pulse = 1.0 + math.sin(frameCount * 0.045) * 0.055
val width = (118 * pulse).toInt
val height = (154 * pulse).toInt
drawGlow(g, x, y, 116, tint, 0.13f)
g.setColor(new Color(7, 17, 39, 235))
g.fillOval(x - width / 2, y - height / 2, width, height)
g.setStroke(new BasicStroke(6.0f))
g.setColor(tint)
g.drawOval(x - width / 2, y - height / 2, width, height)
g.setStroke(new BasicStroke(2.0f))
g.setColor(new Color(193, 245, 255, 175))
g.drawOval(x - width / 2 + 13, y - height / 2 + 14, width - 26, height - 28)
var i = 0
while (i < 8) {
val a = frameCount * 0.022 + i * math.Pi * 2.0 / 8.0
val px = x + (math.cos(a) * 48).toInt
val py = y + (math.sin(a) * 61).toInt
g.setColor(if (i % 2 == 0) C_MINT else C_PINK)
g.fillOval(px - 4, py - 4, 8, 8)
i += 1
}
g.setFont(new Font("Dialog", Font.BOLD, 10))
g.setColor(tint)
drawCentered(g, if (ready) "EXIT OPEN" else "EXIT SEALED", x, y + height / 2 + 20)
}
private def drawPlayer(g: Graphics2D): Unit = {
val x = worldX(playerX)
val y = worldY(playerY)
val bob = math.sin(frameCount * 0.18) * 2.0
if (invulnerability > 0.0 && (frameCount / 5) % 2 == 0) return
drawGlow(g, x, y, 44, C_MINT, 0.13f)
g.setColor(new Color(0, 0, 0, 90))
g.fillOval(x - 19, y + 15, 38, 11)
g.setColor(new Color(34, 55, 91))
g.fillRoundRect(x - 14, y + bob.toInt, 11, 21, 6, 6)
g.fillRoundRect(x + 3, y - bob.toInt, 11, 21, 6, 6)
g.setColor(C_GOLD)
g.fillRoundRect(x - 15, y + 16, 13, 6, 3, 3)
g.fillRoundRect(x + 2, y + 16, 13, 6, 3, 3)
g.setColor(new Color(37, 91, 128))
g.fillRoundRect(x - 19, y - 27 + bob.toInt, 38, 39, 12, 12)
g.setColor(new Color(94, 218, 194))
g.fillRoundRect(x - 14, y - 22 + bob.toInt, 28, 28, 9, 9)
g.setColor(new Color(241, 207, 177))
g.fillOval(x - 12, y - 50 + bob.toInt, 24, 25)
g.setColor(new Color(37, 30, 54))
g.fillArc(x - 13, y - 57 + bob.toInt, 26, 25, 0, 180)
g.setColor(C_WHITE)
g.fillOval(x - 5, y - 40 + bob.toInt, 3, 3)
g.fillOval(x + 3, y - 40 + bob.toInt, 3, 3)
g.setFont(new Font("Dialog", Font.BOLD, 10))
g.setColor(C_WHITE)
drawCentered(g, playerName, x, y - 66)
}
private def drawRemotePlayers(g: Graphics2D): Unit = {
networkPlayers.foreach { peer =>
if (peer.id != localNetworkId) {
val x = worldX(peer.x)
val y = worldY(peer.y)
if (x > -40 && y > -70 && x < 1260 && y < 820) {
drawGlow(g, x, y, 36, C_BLUE, 0.12f)
g.setColor(new Color(40, 91, 150))
g.fillRoundRect(x - 14, y - 25, 28, 40, 10, 10)
g.setColor(C_BLUE)
g.fillRoundRect(x - 10, y - 21, 20, 27, 8, 8)
g.setColor(C_WHITE)
g.fillOval(x - 9, y - 47, 18, 19)
g.setFont(new Font("Dialog", Font.BOLD, 10))
g.setColor(C_GOLD)
drawCentered(g, peer.name, x, y - 56)
}
}
}
}
private def drawGauntlet(g: Graphics2D): Unit = {
val x = worldX(playerX) + 17
val y = worldY(playerY) + 2
g.setColor(new Color(179, 136, 69))
g.fillRoundRect(x - 2, y - 3, 11, 17, 4, 4)
currentGem.foreach { gem =>
drawGlow(g, x + 4, y + 2, 13, gem.tint, 0.24f)
g.setColor(gem.tint)
g.fillOval(x + 1, y - 1, 7, 7)
g.setColor(C_WHITE)
g.drawOval(x + 1, y - 1, 7, 7)
}
}
private def drawSparks(g: Graphics2D): Unit = {
sparks.foreach { p =>
val x = worldX(p.x)
val y = worldY(p.y)
if (x >= -10 && y >= -10 && x < 1930 && y < 1350) {
val alpha = clamp(p.life / 4.0, 0.0, 0.86).toFloat
val old = g.getComposite
g.setComposite(AlphaComposite.SrcOver.derive(alpha))
g.setColor(p.tint)
val size = math.max(2, p.size.toInt)
g.fillOval(x - size / 2, y - size / 2, size, size)
g.setColor(C_WHITE)
g.fillOval(x - 1, y - 1, 2, 2)
g.setComposite(old)
}
}
}
private def drawMiniMap(g: Graphics2D, w: Int): Unit = {
val x = w - 193
val y = 106
val mw = 172
val mh = 118
g.setColor(new Color(4, 12, 28, 225))
g.fillRoundRect(x, y, mw, mh, 14, 14)
g.setColor(new Color(98, 211, 209, 175))
g.drawRoundRect(x, y, mw, mh, 14, 14)
def mx(v: Double): Int = x + 8 + (v / WORLD_W * (mw - 16)).toInt
def my(v: Double): Int = y + 8 + (v / WORLD_H * (mh - 16)).toInt
relics.filterNot(_.collected).foreach { r =>
g.setColor(r.tint)
g.fillOval(mx(r.x) - 3, my(r.y) - 3, 6, 6)
}
g.setColor(C_GOLD)
g.fillOval(mx(PORTAL_X) - 4, my(PORTAL_Y) - 4, 8, 8)
g.setColor(C_WHITE)
g.fillOval(mx(playerX) - 3, my(playerY) - 3, 6, 6)
g.setFont(new Font("Dialog", Font.BOLD, 9))
g.drawString("FIELD SCAN", x + 9, y + 13)
}
private def drawHUD(g: Graphics2D, w: Int, h: Int): Unit = {
g.setColor(new Color(4, 13, 31, 225))
g.fillRoundRect(14, 13, 505, 119, 17, 17)
g.setColor(new Color(89, 208, 199, 170))
g.drawRoundRect(14, 13, 505, 119, 17, 17)
g.setFont(new Font("Dialog", Font.BOLD, 19))
g.setColor(C_MINT)
g.drawString("SANCTUS / SPRING SEASON 04", 29, 41)
g.setFont(new Font("Dialog", Font.BOLD, 11))
g.setColor(C_WHITE)
g.drawString("REALM " + activeRealm + " ? " + currentRealm.name, 29, 63)
g.setColor(C_GOLD)
g.setFont(new Font("Dialog", Font.PLAIN, 10))
g.drawString("SIGILS " + relicCount + " / " + relics.length + " ENEMIES " + aliveEnemyCount + " KOs " + enemyKills, 29, 84)
g.setColor(new Color(35, 57, 77))
g.fillRoundRect(29, 96, 215, 9, 8, 8)
g.setColor(C_MINT)
g.fillRoundRect(29, 96, (215.0 * relicCount / math.max(1, relics.length)).toInt, 9, 8, 8)
g.setColor(C_PINK)
g.drawString("CHARGE " + ("? " * charge), 276, 103)
g.setColor(C_SOFT)
g.setFont(new Font("Dialog", Font.PLAIN, 10))
g.drawString(selectedDifficulty + " / " + selectedMode + " / " + multiplayerMode, 276, 119)
g.setColor(new Color(35, 57, 77))
g.fillRoundRect(29, 111, 215, 7, 7, 7)
g.setColor(C_PINK)
g.fillRoundRect(29, 111, (215.0 * playerHealth / 100.0).toInt, 7, 7, 7)
g.setFont(new Font("Dialog", Font.BOLD, 8))
g.setColor(C_WHITE)
g.drawString("HP " + playerHealth + "/100", 250, 117)
g.setColor(new Color(4, 13, 31, 225))
g.fillRoundRect(w - 178, 16, 158, 67, 14, 14)
g.setFont(new Font("Monospaced", Font.BOLD, 22))
g.setColor(if (secondsLeft < 45.0) C_PINK else C_GOLD)
val mm = (secondsLeft.toInt / 60) % 100
val ss = secondsLeft.toInt % 60
g.drawString(if (selectedMode == "FREE ROAM") "OPEN" else "%02d:%02d".format(mm, ss), w - 157, 48)
g.setFont(new Font("Dialog", Font.PLAIN, 9))
g.setColor(C_WHITE)
g.drawString("TIME WINDOW", w - 155, 68)
g.setColor(new Color(4, 13, 31, 225))
g.fillRoundRect(15, h - 49, w - 30, 35, 12, 12)
g.setFont(new Font("Dialog", Font.BOLD, 10))
g.setColor(C_MINT)
g.drawString("WASD / ARROWS MOVE", 25, h - 27)
g.setColor(C_GOLD)
g.drawString("E COLLECT", 185, h - 27)
g.setColor(C_PINK)
g.drawString("F FIGHT", 270, h - 27)
g.setColor(C_BLUE)
g.drawString("R GEM POWER", 345, h - 27)
g.setColor(C_MINT)
g.drawString("1-5 EQUIP ? C COLLECTION", 455, h - 27)
g.setColor(C_WHITE)
g.drawString("SHIFT DASH ? P PAUSE ? T RETRY ? M REALMS ? O ADMIN ? V CAMERA", 690, h - 27)
g.setColor(new Color(4, 13, 31, 220))
g.fillRoundRect(15, 141, 355, 49, 12, 12)
g.setFont(new Font("Dialog", Font.BOLD, 10))
g.setColor(C_GOLD)
g.drawString("ASTRAL GAUNTLET", 27, 159)
g.setColor(C_WHITE)
g.drawString("GEMS " + collectedGems.size + "/5", 27, 178)
currentGem match {
case Some(gem) =>
g.setColor(gem.tint)
g.drawString(gem.name + " ? " + gem.powerName, 112, 159)
g.setColor(C_SOFT)
g.drawString(if (powerCooldown <= 0.0) "R: READY" else "R: " + math.ceil(powerCooldown).toInt + "s", 112, 178)
case None =>
g.setColor(C_SOFT)
g.drawString("NO GEM EQUIPPED ? COLLECT RUBY FIRST", 112, 168)
}
if (pulseFlash > 0.0) {
val old = g.getComposite
g.setComposite(AlphaComposite.SrcOver.derive((pulseFlash / 0.55 * 0.20).toFloat))
g.setColor(C_BLUE)
g.fillRect(0, 0, w, h)
g.setComposite(old)
}
if (toastTime > 0.0) {
val width = math.min(w - 40, math.max(330, g.getFontMetrics.stringWidth(toast) + 38))
val tx = (w - width) / 2
g.setColor(new Color(4, 15, 34, 235))
g.fillRoundRect(tx, h - 96, width, 32, 14, 14)
g.setColor(C_MINT)
g.drawRoundRect(tx, h - 96, width, 32, 14, 14)
g.setFont(new Font("Dialog", Font.BOLD, 11))
g.setColor(C_WHITE)
drawCentered(g, toast, w / 2, h - 76)
}
if (paused || cleared || failed) drawOverlay(g, w, h)
}
private def drawOverlay(g: Graphics2D, w: Int, h: Int): Unit = {
g.setColor(new Color(3, 8, 23, 215))
g.fillRect(0, 0, w, h)
val title = if (cleared) "REALM STABILIZED" else if (failed) "SIGNAL LOST" else "FIELD PAUSED"
val subtitle = if (cleared) "The exit arch has accepted your sigils." else if (failed) "Restore your charge and make another attempt." else "Your expedition is safely paused."
g.setFont(new Font("Dialog", Font.BOLD, 37))
g.setColor(if (cleared) C_GOLD else C_WHITE)
drawCentered(g, title, w / 2, h / 2 - 35)
g.setFont(new Font("Dialog", Font.PLAIN, 15))
g.setColor(C_SOFT)
drawCentered(g, subtitle, w / 2, h / 2 + 2)
g.setFont(new Font("Dialog", Font.BOLD, 12))
g.setColor(C_MINT)
val instruction = if (cleared && activeRealm < realms.length) "ENTER OPEN NEXT REALM ? M RETURN TO REALMS" else "T RETRY ? M RETURN TO REALMS ? P RESUME"
drawCentered(g, instruction, w / 2, h / 2 + 43)
}
// ---------------------------------------------------------------------------
// POWER GEM COLLECTION
// ---------------------------------------------------------------------------
private def buildCollectionScreen(): JPanel = {
val p = verticalPanel()
addCentered(p, makeTitle("ASTRAL GAUNTLET COLLECTION", 28, C_GOLD), 4)
addCentered(p, makeLabel("Collect each core from defeated enemies. Equip one core at a time.", 12), 5)
collectionButtons.clear()
POWER_GEMS.foreach { gem =>
addCentered(p, makeTitle(gem.name + " ? " + gem.powerName, 15, gem.tint), 16)
addCentered(p, makeLabel(gem.description, 11, C_SOFT), 2)
val b = makeButton("LOCKED ? DEFEAT ENEMIES TO FIND THIS CORE")
collectionButtons += b
addCentered(p, b, 5)
addAction(b) { equipGem(gem.id); refreshCollection() }
}
collectionStatusArea = new JTextArea(3, 48)
collectionStatusArea.setEditable(false)
collectionStatusArea.setFocusable(false)
collectionStatusArea.setBackground(C_PANEL)
collectionStatusArea.setForeground(C_MINT)
collectionStatusArea.setFont(new Font("Monospaced", Font.BOLD, 12))
collectionStatusArea.setBorder(BorderFactory.createLineBorder(C_LINE))
collectionStatusArea.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(12)); p.add(collectionStatusArea)
val back = makeButton("RETURN")
addCentered(p, back, 16)
addAction(back) { returnFromCollection() }
p
}
private def refreshCollection(): Unit = {
var i = 0
while (i < collectionButtons.length && i < POWER_GEMS.length) {
val gem = POWER_GEMS(i)
val owned = collectedGems.contains(gem.id)
collectionButtons(i).setEnabled(owned)
collectionButtons(i).setText(if (!owned) "LOCKED ? DEFEAT AN ENEMY" else if (equippedGem == gem.id) "EQUIPPED ? ACTIVE POWER" else "EQUIP THIS CORE")
i += 1
}
if (collectionStatusArea != null) {
val active = currentGem.map(_.name).getOrElse("NONE")
collectionStatusArea.setText("COLLECTED: " + collectedGems.size + " / 5\nEQUIPPED: " + active + "\nIN GAME: PRESS 1-5 TO EQUIP ? PRESS R TO RELEASE POWER")
}
}
// ---------------------------------------------------------------------------
// GAME CANVAS AND CONTROLS
// ---------------------------------------------------------------------------
private final class RealmCanvas extends JPanel {
setFocusable(true)
setBackground(C_BG)
setPreferredSize(new Dimension(1360, 820))
setDoubleBuffered(true)
addKeyListener(new KeyAdapter {
override def keyPressed(e: KeyEvent): Unit = {
val code = e.getKeyCode
val fresh = !keys.contains(code)
keys += code
if (!fresh) return
if (code == KeyEvent.VK_E) interact()
else if (code == KeyEvent.VK_F) fight()
else if (code == KeyEvent.VK_R) useEquippedPower()
else if (code == KeyEvent.VK_C) showCollection(true)
else if (code >= KeyEvent.VK_1 && code <= KeyEvent.VK_5) equipGem(code - KeyEvent.VK_1)
else if (code == KeyEvent.VK_SPACE) doPulse()
else if (code == KeyEvent.VK_SHIFT && dashCooldown <= 0.0 && started && !paused && !cleared && !failed) {
dashTime = 0.24
dashCooldown = 1.15
burst(playerX, playerY, C_MINT, 9)
}
else if (code == KeyEvent.VK_P || code == KeyEvent.VK_ESCAPE) {
if (!cleared && !failed) paused = !paused
}
else if (code == KeyEvent.VK_T) restartRealm()
else if (code == KeyEvent.VK_O && isAdmin) openAdminPanel(true)
else if (code == KeyEvent.VK_V) {
cameraDistanceIndex = (cameraDistanceIndex + 1) % CAMERA_DISTANCES.length
setToast("3D CAMERA: " + CAMERA_LABELS(cameraDistanceIndex), 1.5)
}
else if (code == KeyEvent.VK_M) returnToRealms()
else if (code == KeyEvent.VK_ENTER && cleared) {
if (activeRealm < realms.length) startRealm(activeRealm + 1)
else returnToRealms()
}
}
override def keyReleased(e: KeyEvent): Unit = keys -= e.getKeyCode
})
addMouseListener(new java.awt.event.MouseAdapter {
override def mousePressed(e: java.awt.event.MouseEvent): Unit = requestFocusInWindow()
})
override protected def paintComponent(graphics: Graphics): Unit = {
super.paintComponent(graphics)
val g = graphics.create().asInstanceOf[Graphics2D]
try {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
val w = getWidth
val h = getHeight
cameraX = clamp(playerX - w / 2.0, 0.0, math.max(0.0, WORLD_W - w))
cameraY = clamp(playerY - h / 2.0, 0.0, math.max(0.0, WORLD_H - h))
// Render the 3D perspective world first, then keep the existing HUD and map in 2D.
draw3DScene(g, w, h)
drawMiniMap(g, w)
drawHUD(g, w, h)
} finally g.dispose()
}
}
// ---------------------------------------------------------------------------
// SCREEN: LOGIN
// ---------------------------------------------------------------------------
private def buildLoginScreen(): JPanel = {
val p = verticalPanel()
p.add(Box.createVerticalGlue())
addCentered(p, makeTitle("SANCTUS", 23, C_MINT), 5)
addCentered(p, makeTitle("SPRING SEASON / FOURTH EVENT", 31, C_WHITE), 4)
addCentered(p, makeTitle("THE LUMEN CIRCUIT", 17, C_GOLD), 3)
addCentered(p, makeLabel("Create a field identity to enter the floating realms.", 14), 17)
val form = new JPanel(new java.awt.GridBagLayout())
form.setOpaque(false)
val c = new java.awt.GridBagConstraints()
c.insets = new java.awt.Insets(8, 8, 8, 8)
c.fill = java.awt.GridBagConstraints.HORIZONTAL
c.gridx = 0; c.gridy = 0
val n = new JLabel("FIELD NAME")
n.setForeground(C_WHITE)
n.setFont(new Font("Dialog", Font.BOLD, 12))
form.add(n, c)
c.gridx = 1
loginNameField = new JTextField("PLAYER", 20)
loginNameField.setFont(new Font("Dialog", Font.PLAIN, 14))
form.add(loginNameField, c)
c.gridx = 0; c.gridy = 1
val pin = new JLabel("ACCESS KEY")
pin.setForeground(C_WHITE)
pin.setFont(new Font("Dialog", Font.BOLD, 12))
form.add(pin, c)
c.gridx = 1
loginPinField = new JPasswordField(20)
form.add(loginPinField, c)
p.add(Box.createVerticalStrut(8))
form.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(form)
val enter = makeButton("ENTER THE CIRCUIT")
addCentered(p, enter, 16)
addCentered(p, makeLabel("Authorized control access is checked privately.", 11, new Color(126, 151, 178)), 8)
p.add(Box.createVerticalGlue())
addAction(enter) {
val typed = Option(loginNameField.getText).getOrElse("").trim.replaceAll("[,;|\\r\\n]", "_").take(18)
val safe = if (typed.isEmpty) "PLAYER" else typed
val pinText = new String(loginPinField.getPassword)
isAdmin = safe.equalsIgnoreCase(ADMIN_NAME) && pinText == ADMIN_PIN
playerName = safe
loginPinField.setText("")
updateWelcome()
if (adminMenuButton != null) adminMenuButton.setVisible(isAdmin)
showCard("menu")
}
p
}
// ---------------------------------------------------------------------------
// SCREEN: MAIN HUB
// ---------------------------------------------------------------------------
private def buildMenuScreen(): JPanel = {
val p = verticalPanel()
p.add(Box.createVerticalGlue())
addCentered(p, makeTitle("SANCTUS EVENT", 35, C_MINT), 2)
addCentered(p, makeTitle("SPRING SEASON / FOURTH EVENT", 16, C_GOLD), 3)
welcomeLabel = makeLabel("Welcome, PLAYER", 14, C_SOFT)
addCentered(p, welcomeLabel, 18)
addCentered(p, makeLabel("Fight enemies. Claim five cores. Master one power at a time.", 13, C_BLUE), 5)
val realmsButton = makeButton("OPEN THE REALM GATE")
val setupButton = makeButton("EXPEDITION SETTINGS")
val collectionButton = makeButton("POWER GEM COLLECTION")
val recordsButton = makeButton("FIELD RECORDS")
hostButton = makeButton("HOST LAN MULTIPLAYER")
joinButton = makeButton("JOIN LAN MULTIPLAYER")
adminMenuButton = makeButton("CONTROL SANCTUM")
val logoutButton = makeButton("LEAVE FIELD")
addCentered(p, realmsButton, 14)
addCentered(p, setupButton, 7)
addCentered(p, collectionButton, 7)
addCentered(p, recordsButton, 7)
addCentered(p, hostButton, 7)
addCentered(p, joinButton, 7)
addCentered(p, adminMenuButton, 7)
addCentered(p, logoutButton, 7)
adminMenuButton.setVisible(false)
p.add(Box.createVerticalGlue())
addAction(realmsButton) { refreshRealmButtons(); showCard("realms") }
addAction(setupButton) { showCard("settings") }
addAction(collectionButton) { showCollection(false) }
addAction(recordsButton) { showRecords() }
addAction(hostButton) { hostLanGame() }
addAction(joinButton) { joinLanGame() }
addAction(adminMenuButton) { openAdminPanel(false) }
addAction(logoutButton) {
stopGame()
closeNetwork()
playerName = "PLAYER"
isAdmin = false
loginNameField.setText("PLAYER")
loginPinField.setText("")
adminMenuButton.setVisible(false)
showCard("login")
}
p
}
private def updateWelcome(): Unit = {
if (welcomeLabel != null) welcomeLabel.setText("Welcome, " + playerName + (if (isAdmin) " ? CONTROLLER" else " ? EXPLORER"))
}
// ---------------------------------------------------------------------------
// SCREEN: REALM SELECT
// ---------------------------------------------------------------------------
private def buildRealmsScreen(): JPanel = {
val p = verticalPanel()
addCentered(p, makeTitle("CHOOSE A REALM", 29, C_MINT), 3)
addCentered(p, makeLabel("Each cleared realm opens the next gate.", 13), 4)
realmButtons.clear()
var i = 0
while (i < realms.length) {
val index = i + 1
addCentered(p, makeTitle("0" + index + " / " + realms(i).name, 17, if (i % 2 == 0) C_GOLD else C_BLUE), 16)
addCentered(p, makeLabel(realms(i).caption, 12, C_SOFT), 3)
val b = makeButton("ENTER REALM " + index)
realmButtons += b
addCentered(p, b, 6)
addCentered(p, makeLabel("SIGILS REQUIRED: " + realms(i).relics + " ? SENTRIES: " + realms(i).sentries, 10, new Color(136, 163, 189)), 2)
i += 1
}
val back = makeButton("RETURN TO HUB")
addCentered(p, back, 18)
p.add(Box.createVerticalGlue())
i = 0
while (i < realmButtons.length) {
val index = i + 1
val b = realmButtons(i)
addAction(b) { startRealm(index) }
i += 1
}
addAction(back) { showCard("menu") }
refreshRealmButtons()
p
}
private def refreshRealmButtons(): Unit = {
var i = 0
while (i < realmButtons.length) {
val number = i + 1
val complete = completedRealms.contains(number)
val open = number == 1 || completedRealms.contains(number - 1)
realmButtons(i).setEnabled(open)
realmButtons(i).setText(if (complete) "REVISIT REALM " + number + " ? STABILIZED" else if (open) "ENTER REALM " + number else "REALM " + number + " ? SEALED")
i += 1
}
}
// ---------------------------------------------------------------------------
// SCREEN: SETTINGS
// ---------------------------------------------------------------------------
private def buildSettingsScreen(): JPanel = {
val p = verticalPanel()
p.add(Box.createVerticalGlue())
addCentered(p, makeTitle("EXPEDITION SETTINGS", 29, C_MINT), 4)
addCentered(p, makeLabel("Tune the field before opening a realm.", 13), 8)
val dRow = new JPanel()
dRow.setOpaque(false)
val dLabel = new JLabel("FIELD RATING ")
dLabel.setForeground(C_WHITE)
dLabel.setFont(new Font("Dialog", Font.BOLD, 12))
val difficultyBox = makeCombo(DIFFICULTIES, selectedDifficulty)
dRow.add(dLabel); dRow.add(difficultyBox)
dRow.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(18)); p.add(dRow)
val mRow = new JPanel()
mRow.setOpaque(false)
val mLabel = new JLabel("RUN FORMAT ")
mLabel.setForeground(C_WHITE)
mLabel.setFont(new Font("Dialog", Font.BOLD, 12))
val modeBox = makeCombo(GAME_MODES, selectedMode)
mRow.add(mLabel); mRow.add(modeBox)
mRow.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(12)); p.add(mRow)
val descriptions = new JTextArea("EASY - first enemy is gentler; longer timer\nMEDIUM - stronger enemies and more health\nHARD - faster enemies and higher damage\nEXTREME - large enemy health and tighter timer\nNIGHTMARE - maximum pressure\n\nADVENTURE - normal realm progress\nTIME ATTACK - shorter clock\nGEM HUNT - extra sigils and enemy drops\nFREE ROAM - no timer or enemies\n\nF: FIGHT R: GEM POWER C: COLLECTION 1-5: EQUIP V: 3D CAMERA DISTANCE")
descriptions.setEditable(false)
descriptions.setFocusable(false)
descriptions.setFont(new Font("Monospaced", Font.PLAIN, 12))
descriptions.setForeground(C_SOFT)
descriptions.setBackground(C_PANEL)
descriptions.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(C_LINE), BorderFactory.createEmptyBorder(12, 15, 12, 15)))
descriptions.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(20)); p.add(descriptions)
val save = makeButton("APPLY SETTINGS")
val back = makeButton("RETURN TO HUB")
addCentered(p, save, 18)
addCentered(p, back, 8)
p.add(Box.createVerticalGlue())
addAction(save) {
selectedDifficulty = difficultyBox.getSelectedItem.toString
selectedMode = modeBox.getSelectedItem.toString
JOptionPane.showMessageDialog(frame, "Expedition settings applied.", "Settings", JOptionPane.INFORMATION_MESSAGE)
}
addAction(back) { showCard("menu") }
p
}
// ---------------------------------------------------------------------------
// SCREEN: CONTROL SANCTUM
// ---------------------------------------------------------------------------
private def buildAdminScreen(): JPanel = {
val p = verticalPanel()
addCentered(p, makeTitle("CONTROL SANCTUM", 29, C_GOLD), 4)
addCentered(p, makeLabel("Press O in a realm to open this panel at any time; select a player or ALL PLAYERS.", 12), 4)
val titleRow = new JPanel(); titleRow.setOpaque(false)
val titleLabel = new JLabel("EVENT TITLE"); titleLabel.setForeground(C_WHITE)
adminTitleField = new JTextField(eventTitle, 24)
titleRow.add(titleLabel); titleRow.add(adminTitleField)
titleRow.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(12)); p.add(titleRow)
val subRow = new JPanel(); subRow.setOpaque(false)
val subLabel = new JLabel("EVENT LINE"); subLabel.setForeground(C_WHITE)
adminSubtitleField = new JTextField(eventSubtitle, 24)
subRow.add(subLabel); subRow.add(adminSubtitleField)
subRow.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(8)); p.add(subRow)
val settingRow = new JPanel(); settingRow.setOpaque(false)
adminDifficultyBox = makeCombo(DIFFICULTIES, selectedDifficulty)
adminModeBox = makeCombo(GAME_MODES, selectedMode)
settingRow.add(new JLabel("DIFFICULTY")); settingRow.add(adminDifficultyBox)
settingRow.add(new JLabel("MODE")); settingRow.add(adminModeBox)
settingRow.getComponents.foreach { case l: JLabel => l.setForeground(C_WHITE); l.setFont(new Font("Dialog", Font.BOLD, 11)); case _ => () }
settingRow.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(8)); p.add(settingRow)
val targetRow = new JPanel(); targetRow.setOpaque(false)
val targetLabel = new JLabel("TARGET PLAYER"); targetLabel.setForeground(C_WHITE)
adminTargetBox = new JComboBox[String]()
adminTargetBox.setPreferredSize(new Dimension(310, 28))
targetRow.add(targetLabel); targetRow.add(adminTargetBox)
targetRow.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(14)); p.add(targetRow)
val commandPanel = new JPanel(new java.awt.GridLayout(2, 3, 8, 8))
commandPanel.setOpaque(false)
val heal = makeButton("HEAL")
val gems = makeButton("GRANT 5 GEMS")
val boost = makeButton("SPEED BOOST")
val freeze = makeButton("FREEZE ENEMIES")
val clear = makeButton("CLEAR ENEMIES")
val revive = makeButton("REVIVE")
commandPanel.add(heal); commandPanel.add(gems); commandPanel.add(boost)
commandPanel.add(freeze); commandPanel.add(clear); commandPanel.add(revive)
commandPanel.setMaximumSize(new Dimension(720, 104))
commandPanel.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(10)); p.add(commandPanel)
addAction(heal) { sendAdminAction("HEAL") }
addAction(gems) { sendAdminAction("GEMS") }
addAction(boost) { sendAdminAction("BOOST") }
addAction(freeze) { sendAdminAction("FREEZE") }
addAction(clear) { sendAdminAction("CLEAR") }
addAction(revive) { sendAdminAction("REVIVE") }
adminStatusArea = new JTextArea(10, 55)
adminStatusArea.setEditable(false)
adminStatusArea.setFont(new Font("Monospaced", Font.PLAIN, 12))
adminStatusArea.setForeground(C_MINT)
adminStatusArea.setBackground(new Color(4, 13, 30))
adminStatusArea.setBorder(BorderFactory.createLineBorder(C_LINE))
val scroller = new JScrollPane(adminStatusArea)
scroller.setAlignmentX(Component.CENTER_ALIGNMENT)
p.add(Box.createVerticalStrut(12)); p.add(scroller)
val save = makeButton("SAVE EVENT CONFIGURATION")
val refresh = makeButton("REFRESH PLAYER LIST")
val back = makeButton("RETURN TO HUB")
addCentered(p, save, 12); addCentered(p, refresh, 7); addCentered(p, back, 7)
p.add(Box.createVerticalGlue())
addAction(save) {
val t = Option(adminTitleField.getText).getOrElse("").trim
val sub = Option(adminSubtitleField.getText).getOrElse("").trim
if (t.nonEmpty) eventTitle = t.take(48)
if (sub.nonEmpty) eventSubtitle = sub.take(64)
selectedDifficulty = adminDifficultyBox.getSelectedItem.toString
selectedMode = adminModeBox.getSelectedItem.toString
if (started) {
// Keep the host and LAN clients on the same realm configuration.
buildRealm()
if (gameCanvas != null) gameCanvas.requestFocusInWindow()
}
refreshAdmin()
JOptionPane.showMessageDialog(frame, "Configuration saved for this session." + (if (started) " The current realm has been reset to apply the new settings." else ""), "Control Sanctum", JOptionPane.INFORMATION_MESSAGE)
}
addAction(refresh) { refreshAdmin() }
addAction(back) { closeAdminPanel() }
refreshAdmin()
p
}
private def refreshAdminTargets(): Unit = {
if (adminTargetBox != null) {
val desired = Vector("ALL PLAYERS") ++ networkPlayers.map(peer => peer.id + " ? " + peer.name)
val current = (0 until adminTargetBox.getItemCount).map(i => adminTargetBox.getItemAt(i)).toVector
// Do not rebuild the combo while its popup is open unless the roster changed.
if (current != desired) {
val old = Option(adminTargetBox.getSelectedItem).map(_.toString).getOrElse("ALL PLAYERS")
adminTargetBox.removeAllItems()
desired.foreach(item => adminTargetBox.addItem(item))
val idx = (0 until adminTargetBox.getItemCount).find(i => adminTargetBox.getItemAt(i) == old).getOrElse(0)
adminTargetBox.setSelectedIndex(idx)
}
}
}
private def refreshAdmin(): Unit = {
if (adminTitleField != null) adminTitleField.setText(eventTitle)
if (adminSubtitleField != null) adminSubtitleField.setText(eventSubtitle)
if (adminDifficultyBox != null) adminDifficultyBox.setSelectedItem(selectedDifficulty)
if (adminModeBox != null) adminModeBox.setSelectedItem(selectedMode)
refreshAdminTargets()
if (adminStatusArea != null) {
val rows = ArrayBuffer[String]()
rows += "SPRING SEASON / FOURTH EVENT"
rows += "EVENT TITLE : " + eventTitle
rows += "CONTROLLER : " + (if (isAdmin) ADMIN_NAME else "PLAYER")
rows += "RATING : " + selectedDifficulty
rows += "FORMAT : " + selectedMode
rows += "NETWORK : " + multiplayerMode
rows += "CONNECTED : " + networkPlayers.length + " PLAYER(S)"
rows += ""
rows += "CONNECTED PLAYER ROSTER"
if (networkPlayers.isEmpty) rows += "No LAN players in this session. Host a room or join one from the hub."
networkPlayers.foreach { peer => rows += (peer.id + " " + peer.name + " HP " + peer.hp + " REALM " + peer.realm) }
rows += ""
rows += "ADMIN ACTIONS: HEAL / GEMS / BOOST / FREEZE / CLEAR / REVIVE"
rows += "Changes remain active until the application closes."
adminStatusArea.setText(rows.mkString("\n"))
}
}
// ---------------------------------------------------------------------------
// FIELD RECORDS
// ---------------------------------------------------------------------------
private def showRecords(): Unit = {
val text = new StringBuilder
text.append("SANCTUS EVENT / FIELD RECORDS\n\n")
text.append("Explorer: ").append(playerName).append("\n")
text.append("Stabilized realms: ").append(completedRealms.size).append(" / ").append(realms.length).append("\n\n")
realms.indices.foreach { i =>
text.append("0").append(i + 1).append(" ").append(realms(i).name).append(" ? ")
text.append(if (completedRealms.contains(i + 1)) "STABILIZED" else "NOT YET STABILIZED").append("\n")
}
text.append("\nPower gems collected: ").append(collectedGems.size).append(" / 5")
text.append("\nEnemy defeats this realm: ").append(enemyKills)
text.append("\nEquipped core: ").append(currentGem.map(_.name).getOrElse("NONE"))
text.append("\nCurrent rating: ").append(selectedDifficulty)
text.append("\nCurrent format: ").append(selectedMode)
JOptionPane.showMessageDialog(frame, text.toString, "Field Records", JOptionPane.INFORMATION_MESSAGE)
}
// ---------------------------------------------------------------------------
// WINDOW INITIALIZATION
// ---------------------------------------------------------------------------
private def buildUI(): Unit = {
cardLayout = new java.awt.CardLayout()
root = new JPanel(cardLayout)
root.setBackground(C_BG)
root.add(scrollCard(buildLoginScreen()), "login")
root.add(scrollCard(buildMenuScreen()), "menu")
root.add(scrollCard(buildRealmsScreen()), "realms")
root.add(scrollCard(buildCollectionScreen()), "collection")
root.add(scrollCard(buildSettingsScreen()), "settings")
root.add(scrollCard(buildAdminScreen()), "admin")
frame = new JFrame(APP_TITLE)
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE)
frame.setContentPane(root)
frame.addWindowListener(new WindowAdapter {
override def windowClosing(e: WindowEvent): Unit = {
stopGame()
if (networkAdminTimer != null) networkAdminTimer.stop()
closeNetwork()
frame.dispose()
}
})
frame.setMinimumSize(new Dimension(900, 650))
frame.setSize(1180, 790)
frame.setLocationRelativeTo(null)
frame.setVisible(true)
networkAdminTimer = new Timer(120, new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
consumeNetworkAdminCommands()
consumeNetworkActions()
if (adminStatusArea != null && root != null && cardLayout != null) {
// Refresh only the target-player list; do not overwrite text being edited.
if (adminTargetBox != null && adminTargetBox.isShowing) refreshAdminTargets()
}
}
})
networkAdminTimer.start()
showCard("login")
}
def main(args: Array[String]): Unit = {
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = buildUI()
})
}
}
SanctusFourthEvent.main(Array[String]())