Code Sketch
yoiiiiiii
Category: Programming
import java.io.{DataInputStream, DataOutputStream}
import java.net.{DatagramPacket, DatagramSocket, Inet4Address, InetAddress, InetSocketAddress, NetworkInterface, ServerSocket, Socket, SocketTimeoutException}
import javax.net.ssl.{SSLServerSocketFactory, SSLSocketFactory}
import java.nio.charset.StandardCharsets
import java.util.Base64
object GameProtocol {
val DefaultPort = 43871
val ProtocolVersion = "STEAL-EGG-YADNESh-2-GUARD-AI"
def enc(s: String): String = Base64.getUrlEncoder.withoutPadding()
.encodeToString(Option(s).getOrElse("").getBytes(StandardCharsets.UTF_8))
def dec(s: String): String = try {
new String(Base64.getUrlDecoder.decode(s), StandardCharsets.UTF_8)
} catch { case _: Throwable => "" }
def pack(command: String, args: String*): String =
(command +: args.map(enc)).mkString("|")
def unpack(line: String): (String, Vector[String]) = {
val p = line.split("\\|", -1).toVector
if (p.isEmpty) ("", Vector.empty)
else (p.head, p.drop(1).map(dec))
}
def serverSocket(port: Int): ServerSocket = {
if (java.lang.Boolean.getBoolean("stealegg.tls"))
SSLServerSocketFactory.getDefault.createServerSocket(port)
else new ServerSocket(port)
}
def clientSocket(host: String, port: Int): Socket = {
if (!java.lang.Boolean.getBoolean("stealegg.tls")) new Socket(host, port)
else {
val s = SSLSocketFactory.getDefault.createSocket(host, port)
.asInstanceOf[javax.net.ssl.SSLSocket]
val p = s.getSSLParameters
p.setEndpointIdentificationAlgorithm("HTTPS")
s.setSSLParameters(p)
s.startHandshake()
s
}
}
// LAN discovery works only on the same local network. It does not discover
// a server across the public Internet or bypass router/firewall rules.
val DiscoveryPort = 43872
private val DiscoveryRequest = "STEAL_EGG_DISCOVER_V1"
private val DiscoveryResponse = "STEAL_EGG_SERVER_V1"
def localIPv4Address(): String = {
val found = scala.collection.mutable.ArrayBuffer.empty[String]
try {
val interfaces = NetworkInterface.getNetworkInterfaces
while (interfaces != null && interfaces.hasMoreElements) {
val ni = interfaces.nextElement()
if (ni.isUp && !ni.isLoopback) {
val addresses = ni.getInetAddresses
while (addresses.hasMoreElements) {
val address = addresses.nextElement()
if (address.isInstanceOf[Inet4Address] && !address.isLoopbackAddress)
found += address.getHostAddress
}
}
}
} catch { case _: Exception => () }
found.headOption.getOrElse("127.0.0.1")
}
def startDiscoveryResponder(serverPort: Int): Thread = {
val t = new Thread(new Runnable {
override def run(): Unit = {
var socket: DatagramSocket = null
try {
socket = new DatagramSocket(null)
socket.setReuseAddress(true)
socket.bind(new InetSocketAddress(DiscoveryPort))
socket.setBroadcast(true)
while (!socket.isClosed) {
val bytes = new Array[Byte](128)
val request = new DatagramPacket(bytes, bytes.length)
socket.receive(request)
val message = new String(request.getData, request.getOffset,
request.getLength, StandardCharsets.UTF_8).trim
if (message == DiscoveryRequest) {
val response = (DiscoveryResponse + "|" + serverPort + "|" + ProtocolVersion)
.getBytes(StandardCharsets.UTF_8)
socket.send(new DatagramPacket(response, response.length,
request.getAddress, request.getPort))
}
}
} catch {
case _: java.net.SocketException => ()
case e: Exception => System.err.println("LAN discovery stopped: " + e.getMessage)
} finally {
if (socket != null) socket.close()
}
}
}, "steal-egg-lan-discovery")
t.setDaemon(true)
t.start()
t
}
def discoverServer(timeoutMillis: Int = 1800): Option[(String, Int)] = {
var socket: DatagramSocket = null
try {
socket = new DatagramSocket()
socket.setBroadcast(true)
socket.setSoTimeout(250)
val payload = DiscoveryRequest.getBytes(StandardCharsets.UTF_8)
val broadcasts = scala.collection.mutable.ArrayBuffer.empty[InetAddress]
broadcasts += InetAddress.getByName("255.255.255.255")
try {
val interfaces = NetworkInterface.getNetworkInterfaces
while (interfaces != null && interfaces.hasMoreElements) {
val ni = interfaces.nextElement()
if (ni.isUp && !ni.isLoopback) {
val addresses = ni.getInterfaceAddresses
val it = addresses.iterator()
while (it.hasNext) {
val broadcast = it.next().getBroadcast
if (broadcast != null) broadcasts += broadcast
}
}
}
} catch { case _: Exception => () }
broadcasts.distinct.foreach { address =>
try socket.send(new DatagramPacket(payload, payload.length, address, DiscoveryPort))
catch { case _: Exception => () }
}
val deadline = System.nanoTime() + timeoutMillis.toLong * 1000000L
while (System.nanoTime() < deadline) {
val bytes = new Array[Byte](256)
val packet = new DatagramPacket(bytes, bytes.length)
try {
socket.receive(packet)
val message = new String(packet.getData, packet.getOffset,
packet.getLength, StandardCharsets.UTF_8).trim
val parts = message.split("\\|", -1)
if (parts.length == 3 && parts(0) == DiscoveryResponse && parts(2) == ProtocolVersion) {
val discoveredPort = scala.util.Try(parts(1).toInt).getOrElse(0)
if (discoveredPort >= 1 && discoveredPort <= 65535)
return Some((packet.getAddress.getHostAddress, discoveredPort))
}
} catch {
case _: SocketTimeoutException => ()
}
}
None
} catch {
case _: Exception => None
} finally {
if (socket != null) socket.close()
}
}
case class PlayerView(name: String, x: Double, y: Double,
baseX: Double, baseY: Double, role: String)
case class GuardView(id: String, biome: String, x: Double, y: Double,
chasing: Boolean)
case class EggView(id: String, name: String, rarity: String,
biome: String, x: Double, y: Double)
case class EggItem(id: String, name: String, rarity: String,
biome: String, hatchAt: Long)
case class PetView(name: String, rarity: String, income: Long)
case class Snapshot(coins: Long, speed: Int, role: String,
baseLevel: Int, x: Double, y: Double,
eventTitle: String, eventText: String,
announcement: String, updateNote: String,
boostMultiplier: Double, boostUntil: Long,
players: Vector[PlayerView], guards: Vector[GuardView],
worldEggs: Vector[EggView], eggs: Vector[EggItem],
pets: Vector[PetView])
def writeSnapshot(out: DataOutputStream, s: Snapshot): Unit = {
out.writeUTF("STATE")
out.writeLong(s.coins); out.writeInt(s.speed); out.writeUTF(s.role)
out.writeInt(s.baseLevel); out.writeDouble(s.x); out.writeDouble(s.y)
out.writeUTF(s.eventTitle); out.writeUTF(s.eventText)
out.writeUTF(s.announcement); out.writeUTF(s.updateNote)
out.writeDouble(s.boostMultiplier); out.writeLong(s.boostUntil)
out.writeInt(s.players.size)
s.players.foreach { p =>
out.writeUTF(p.name); out.writeDouble(p.x); out.writeDouble(p.y)
out.writeDouble(p.baseX); out.writeDouble(p.baseY); out.writeUTF(p.role)
}
out.writeInt(s.guards.size)
s.guards.foreach { guard =>
out.writeUTF(guard.id); out.writeUTF(guard.biome)
out.writeDouble(guard.x); out.writeDouble(guard.y)
out.writeBoolean(guard.chasing)
}
out.writeInt(s.worldEggs.size)
s.worldEggs.foreach { e =>
out.writeUTF(e.id); out.writeUTF(e.name); out.writeUTF(e.rarity)
out.writeUTF(e.biome); out.writeDouble(e.x); out.writeDouble(e.y)
}
out.writeInt(s.eggs.size)
s.eggs.foreach { e =>
out.writeUTF(e.id); out.writeUTF(e.name); out.writeUTF(e.rarity)
out.writeUTF(e.biome); out.writeLong(e.hatchAt)
}
out.writeInt(s.pets.size)
s.pets.foreach { p =>
out.writeUTF(p.name); out.writeUTF(p.rarity); out.writeLong(p.income)
}
out.flush()
}
def readSnapshot(in: DataInputStream): Snapshot = {
val marker = in.readUTF()
if (marker != "STATE") throw new java.io.IOException("Invalid state packet")
val coins = in.readLong(); val speed = in.readInt(); val role = in.readUTF()
val baseLevel = in.readInt(); val x = in.readDouble(); val y = in.readDouble()
val eventTitle = in.readUTF(); val eventText = in.readUTF()
val announcement = in.readUTF(); val updateNote = in.readUTF()
val boost = in.readDouble(); val boostUntil = in.readLong()
val pc = math.max(0, math.min(64, in.readInt()))
val players = Vector.fill(pc) {
PlayerView(in.readUTF(), in.readDouble(), in.readDouble(),
in.readDouble(), in.readDouble(), in.readUTF())
}
val gc = math.max(0, math.min(128, in.readInt()))
val guards = Vector.fill(gc) {
GuardView(in.readUTF(), in.readUTF(), in.readDouble(), in.readDouble(), in.readBoolean())
}
val ec = math.max(0, math.min(256, in.readInt()))
val worldEggs = Vector.fill(ec) {
EggView(in.readUTF(), in.readUTF(), in.readUTF(), in.readUTF(),
in.readDouble(), in.readDouble())
}
val ic = math.max(0, math.min(128, in.readInt()))
val eggs = Vector.fill(ic) {
EggItem(in.readUTF(), in.readUTF(), in.readUTF(), in.readUTF(), in.readLong())
}
val petCount = math.max(0, math.min(128, in.readInt()))
val pets = Vector.fill(petCount) {
PetView(in.readUTF(), in.readUTF(), in.readLong())
}
Snapshot(coins, speed, role, baseLevel, x, y, eventTitle, eventText,
announcement, updateNote, boost, boostUntil, players, guards, worldEggs, eggs, pets)
}
}
import java.io._
import java.net._
import java.security.SecureRandom
import java.util.Properties
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import scala.collection.mutable
import scala.util.Try
import GameProtocol._
/**
* Authoritative game server. Run with: sbt "runMain StealAnEggServer"
* LAN prototype is enabled by default. For public internet use TLS and a trusted
* certificate: set -Dstealegg.tls=true and configure the JVM keyStore/trustStore.
*/
object StealAnEggServer {
private val port = Try(sys.props.getOrElse("stealegg.port", DefaultPort.toString).toInt)
.getOrElse(DefaultPort)
private val dataDir = new File(System.getProperty("user.home"), ".steal_an_egg_yadnesh")
private val accountFile = new File(dataDir, "accounts.properties")
private val rng = new SecureRandom()
private val lock = new Object
private val MaxPlayers = 10
private val accounts = mutable.Map.empty[String, Account]
private val sessions = mutable.Map.empty[String, Session]
private val worldEggs = mutable.ArrayBuffer.empty[WorldEgg]
private var announcement = "Welcome! Find an egg, hatch a pet and grow your base."
private var eventTitle = "SPRING EGG FESTIVAL"
private var eventText = "A new adventure begins!"
private var updateNote = "Game build 0.1.0"
private var boostMultiplier = 1.0
private var boostUntil = 0L
// Owner bootstrap credential requested for this private game prototype.
// Override it before public hosting with -Dstealegg.owner.password=YOUR_NEW_SECRET
// or the STEAL_EGG_OWNER_PASSWORD environment variable.
private val ownerBootstrapPassword =
sys.props.get("stealegg.owner.password")
.orElse(Option(System.getenv("STEAL_EGG_OWNER_PASSWORD")))
.filter(_.nonEmpty)
.getOrElse("1772admin")
@volatile private var running = true
private case class Biome(name: String, gate: Int, xStart: Double, color: String)
private val biomes = Vector(
Biome("Green Meadow", 0, 0, "meadow"),
Biome("Whispering Forest", 5, 24, "forest"),
Biome("Crystal Lake", 10, 48, "crystal-lake"),
Biome("Golden Desert", 15, 72, "desert"),
Biome("Mystic Jungle", 20, 96, "jungle"),
Biome("Savannah", 30, 120, "savannah"),
Biome("Sakura Valley", 40, 144, "sakura"),
Biome("Frost Peaks", 50, 168, "frost"),
Biome("Ice Cavern", 60, 192, "ice"),
Biome("Candy Fields", 70, 216, "candy"),
Biome("Mushroom Woods", 85, 240, "mushroom"),
Biome("Coral Reef", 100, 264, "coral"),
Biome("Deep Ocean", 120, 288, "deep-ocean"),
Biome("Volcanic Crater", 140, 312, "volcano"),
Biome("Prehistoric Wilds", 165, 336, "prehistoric"),
Biome("Sky Islands", 190, 360, "sky"),
Biome("Cloud Kingdom", 220, 384, "cloud"),
Biome("Crystal Caverns", 250, 408, "crystal"),
Biome("Cyber City", 285, 432, "cyber"),
Biome("Cosmic Realm", 320, 456, "cosmic"),
Biome("Void Dimension", 360, 480, "void")
)
private case class EggDef(name: String, rarity: String, biome: String, income: Long)
private val eggDefs = Vector(
EggDef("Meadow Egg", "Common", "Green Meadow", 2),
EggDef("Forest Egg", "Uncommon", "Whispering Forest", 4),
EggDef("Crystal Lake Egg", "Rare", "Crystal Lake", 7),
EggDef("Dune Egg", "Rare", "Golden Desert", 12),
EggDef("Tiger Egg", "Epic", "Mystic Jungle", 18),
EggDef("Savannah Egg", "Epic", "Savannah", 25),
EggDef("Sakura Egg", "Epic", "Sakura Valley", 34),
EggDef("Frost Egg", "Legendary", "Frost Peaks", 48),
EggDef("Glacier Egg", "Legendary", "Ice Cavern", 62),
EggDef("Candy Egg", "Legendary", "Candy Fields", 82),
EggDef("Mushroom Egg", "Mythic", "Mushroom Woods", 110),
EggDef("Coral Egg", "Mythic", "Coral Reef", 145),
EggDef("Abyss Egg", "Mythic", "Deep Ocean", 190),
EggDef("Phoenix Egg", "Mythic", "Volcanic Crater", 250),
EggDef("Dino Egg", "Secret", "Prehistoric Wilds", 330),
EggDef("Sky Egg", "Secret", "Sky Islands", 430),
EggDef("Cloud Egg", "Secret", "Cloud Kingdom", 560),
EggDef("Prism Egg", "Secret", "Crystal Caverns", 720),
EggDef("Cyber Egg", "Celestial", "Cyber City", 920),
EggDef("Galaxy Egg", "Celestial", "Cosmic Realm", 1200),
EggDef("Void Egg", "Celestial", "Void Dimension", 1600)
)
private class Account(val username: String, var salt: Array[Byte], var hash: Array[Byte],
var coins: Long, var speed: Int, var role: String,
var baseLevel: Int, val baseX: Double, val baseY: Double) {
val eggs = mutable.ArrayBuffer.empty[EggItem]
val pets = mutable.ArrayBuffer.empty[PetView]
var lastIncome = System.currentTimeMillis()
}
private case class WorldEgg(id: String, name: String, rarity: String,
biome: String, x: Double, y: Double, income: Long)
private class GuardNpc(val id: String, val biome: String,
val homeX: Double, val homeY: Double,
val phase: Double) {
var x: Double = homeX
var y: Double = homeY
var chasing: Boolean = false
}
private class Session(val socket: Socket, val in: DataInputStream,
val out: DataOutputStream, val account: Account,
var x: Double, var y: Double) {
var lastTrain = 0L
var lastMove = System.currentTimeMillis()
var lastGuardUpdate = System.currentTimeMillis()
var guardLootStartedAt = 0L
val guardLootIds = mutable.LinkedHashSet.empty[String]
val guards = mutable.ArrayBuffer.empty[GuardNpc]
}
private def safeName(s: String): Boolean =
s != null && s.matches("[A-Za-z0-9_]{3,18}")
private def norm(s: String): String = s.toLowerCase(java.util.Locale.ROOT)
private def randomBytes(n: Int): Array[Byte] = {
val b = new Array[Byte](n); rng.nextBytes(b); b
}
private def hashPassword(password: String, salt: Array[Byte]): Array[Byte] = {
val spec = new PBEKeySpec(password.toCharArray, salt, 160000, 256)
try SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded
finally spec.clearPassword()
}
private def verifyPassword(a: Account, password: String): Boolean = {
val actual = hashPassword(password, a.salt)
java.security.MessageDigest.isEqual(actual, a.hash)
}
private def freshBase(name: String): (Double, Double) = {
val h = math.abs(name.hashCode.toLong)
(3.0 + (h % 5).toDouble * 2.2, 3.0 + ((h / 5) % 4).toDouble * 2.0)
}
private def encodeRecord(fields: String*): String = fields.map(GameProtocol.enc).mkString("~")
private def decodeRecord(s: String): Vector[String] = s.split("~", -1).toVector.map(GameProtocol.dec)
private def loadAccounts(): Unit = lock.synchronized {
if (accountFile.exists()) {
val p = new Properties()
val in = new FileInputStream(accountFile)
try p.load(in) finally in.close()
val count = Try(p.getProperty("count", "0").toInt).getOrElse(0).max(0).min(10000)
for (i <- 0 until count) {
val pre = "a." + i + "."
val name = p.getProperty(pre + "name", "")
if (name.nonEmpty) {
val salt = Try(java.util.Base64.getDecoder.decode(p.getProperty(pre + "salt", ""))).getOrElse(Array.emptyByteArray)
val hash = Try(java.util.Base64.getDecoder.decode(p.getProperty(pre + "hash", ""))).getOrElse(Array.emptyByteArray)
if (salt.nonEmpty && hash.nonEmpty) {
val (bx, by) = freshBase(name)
val a = new Account(name, salt, hash,
Try(p.getProperty(pre + "coins", "100").toLong).getOrElse(100L),
Try(p.getProperty(pre + "speed", "1").toInt).getOrElse(1),
p.getProperty(pre + "role", "PLAYER"),
Try(p.getProperty(pre + "baseLevel", "1").toInt).getOrElse(1),
Try(p.getProperty(pre + "baseX", bx.toString).toDouble).getOrElse(bx),
Try(p.getProperty(pre + "baseY", by.toString).toDouble).getOrElse(by)
)
p.getProperty(pre + "eggs", "").split(";").filter(_.nonEmpty).foreach { rec =>
val f = decodeRecord(rec)
if (f.size == 5) a.eggs += EggItem(f(0), f(1), f(2), f(3), Try(f(4).toLong).getOrElse(0L))
}
p.getProperty(pre + "pets", "").split(";").filter(_.nonEmpty).foreach { rec =>
val f = decodeRecord(rec)
if (f.size == 3) a.pets += PetView(f(0), f(1), Try(f(2).toLong).getOrElse(1L))
}
accounts(norm(name)) = a
}
}
}
}
}
private def saveAccounts(): Unit = lock.synchronized {
dataDir.mkdirs()
val p = new Properties()
val sorted = accounts.values.toVector.sortBy(_.username.toLowerCase(java.util.Locale.ROOT))
p.setProperty("count", sorted.size.toString)
sorted.zipWithIndex.foreach { case (a, i) =>
val pre = "a." + i + "."
p.setProperty(pre + "name", a.username)
p.setProperty(pre + "salt", java.util.Base64.getEncoder.encodeToString(a.salt))
p.setProperty(pre + "hash", java.util.Base64.getEncoder.encodeToString(a.hash))
p.setProperty(pre + "coins", a.coins.toString)
p.setProperty(pre + "speed", a.speed.toString)
p.setProperty(pre + "role", a.role)
p.setProperty(pre + "baseLevel", a.baseLevel.toString)
p.setProperty(pre + "baseX", a.baseX.toString)
p.setProperty(pre + "baseY", a.baseY.toString)
p.setProperty(pre + "eggs", a.eggs.map(e => encodeRecord(e.id, e.name, e.rarity, e.biome, e.hatchAt.toString)).mkString(";"))
p.setProperty(pre + "pets", a.pets.map(e => encodeRecord(e.name, e.rarity, e.income.toString)).mkString(";"))
}
val tmp = new File(dataDir, "accounts.properties.tmp")
val out = new FileOutputStream(tmp)
try p.store(out, "Steal An Egg - Yadnesh Edition accounts") finally out.close()
try {
java.nio.file.Files.move(tmp.toPath, accountFile.toPath,
java.nio.file.StandardCopyOption.REPLACE_EXISTING,
java.nio.file.StandardCopyOption.ATOMIC_MOVE)
} catch {
case _: java.nio.file.AtomicMoveNotSupportedException =>
java.nio.file.Files.move(tmp.toPath, accountFile.toPath,
java.nio.file.StandardCopyOption.REPLACE_EXISTING)
}
}
private def eggId(): String = java.util.UUID.randomUUID().toString.take(12)
private def spawnEgg(biomeName: String, rarityOverride: String = ""): Unit = {
val idx = biomes.indexWhere(_.name.equalsIgnoreCase(biomeName))
if (idx < 0) return
val choices = eggDefs.filter(_.biome.equalsIgnoreCase(biomeName))
val base = choices.headOption.getOrElse(eggDefs.head)
val rarity = if (rarityOverride.nonEmpty) rarityOverride else base.rarity
val x = biomes(idx).xStart + 4.0 + rng.nextDouble() * 14.0
val y = 5.0 + rng.nextDouble() * 9.0
worldEggs += WorldEgg(eggId(), base.name, rarity, biomeName, x, y, base.income)
}
private def seedWorld(): Unit = lock.synchronized {
if (worldEggs.nonEmpty) return
biomes.foreach { b =>
val count = if (b.name == "Forest") 5 else 3
for (_ <- 0 until count) spawnEgg(b.name)
}
}
private def biomeIndex(x: Double): Int =
math.max(0, math.min(biomes.size - 1, (x / 24.0).toInt))
private def canEnter(a: Account, x: Double): Boolean =
a.speed >= biomes(biomeIndex(x)).gate
private def nearestBase(x: Double, y: Double, target: Account): Boolean =
math.hypot(x - target.baseX, y - target.baseY) <= 3.0
private def initialiseGuards(s: Session): Unit = {
biomes.zipWithIndex.foreach { case (b, i) =>
s.guards += new GuardNpc("guard-" + i + "-A", b.name,
b.xStart + 7.2, 5.2, i * 0.73)
s.guards += new GuardNpc("guard-" + i + "-B", b.name,
b.xStart + 17.0, 15.0, i * 0.73 + 2.1)
}
}
private def alertGuardsInBiome(s: Session, biomeName: String): Unit =
s.guards.filter(_.biome.equalsIgnoreCase(biomeName)).foreach(g => g.chasing = true)
// Server-authoritative guard patrol, pursuit and egg-recovery logic.
private def updateGuardAI(s: Session, now: Long): Unit = {
val dt = math.max(0.01, math.min(0.25, (now - s.lastGuardUpdate).toDouble / 1000.0))
s.lastGuardUpdate = now
val safelyHome = math.hypot(s.x - s.account.baseX, s.y - s.account.baseY) <= 4.2
if (safelyHome && s.guardLootIds.nonEmpty && now - s.guardLootStartedAt > 1400L) {
s.guardLootIds.clear()
s.guards.foreach(g => g.chasing = false)
announcement = s.account.username + " escaped the animal guards and reached the safe base!"
}
// Guards in every new biome notice a player who is still carrying an egg.
if (s.guardLootIds.nonEmpty) {
val currentBiome = biomes(biomeIndex(s.x)).name
alertGuardsInBiome(s, currentBiome)
}
s.guards.foreach { guard =>
if (guard.chasing && s.guardLootIds.nonEmpty) {
val dx = s.x - guard.x
val dy = s.y - guard.y
val distance = math.hypot(dx, dy)
if (distance > 0.001) {
val step = math.min(distance, 6.4 * dt)
guard.x += dx / distance * step
guard.y += dy / distance * step
}
if (math.hypot(s.x - guard.x, s.y - guard.y) <= 1.45 && s.guardLootIds.nonEmpty) {
val recoveredId = s.guardLootIds.head
val eggIndex = s.account.eggs.indexWhere(_.id == recoveredId)
if (eggIndex >= 0) {
val recovered = s.account.eggs.remove(eggIndex)
val income = eggDefs.find(_.name == recovered.name).map(_.income).getOrElse(3L)
val dropX = math.max(0.8, math.min(biomes.size * 24.0 - 1.0, s.x + 1.8))
val dropY = math.max(1.2, math.min(18.8, s.y + 1.1))
worldEggs += WorldEgg(recovered.id, recovered.name, recovered.rarity,
recovered.biome, dropX, dropY, income)
announcement = "ANIMAL GUARD CAUGHT " + s.account.username + "! THE EGG WAS RETURNED!"
saveAccounts()
} else {
announcement = "ANIMAL GUARD TAGGED " + s.account.username + "! RUN BACK TO YOUR BASE!"
}
// One successful guard catch recovers one egg and ends this chase.
s.guardLootIds.clear()
s.guards.foreach(g => g.chasing = false)
}
} else {
// Calm patrol route with a small elliptical walk around each guard post.
val phaseNow = now.toDouble / 1250.0 + guard.phase
val targetX = guard.homeX + math.sin(phaseNow) * 1.35
val targetY = guard.homeY + math.cos(phaseNow * 0.72) * 1.65
val dx = targetX - guard.x
val dy = targetY - guard.y
val distance = math.hypot(dx, dy)
if (distance > 0.03) {
val step = math.min(distance, 1.25 * dt)
guard.x += dx / distance * step
guard.y += dy / distance * step
}
}
}
}
private def buildSnapshot(s: Session): Snapshot = lock.synchronized {
val now = System.currentTimeMillis()
updateGuardAI(s, now)
val players = sessions.values.toVector.map { p =>
PlayerView(p.account.username, p.x, p.y, p.account.baseX, p.account.baseY, p.account.role)
}
val guards = s.guards.map(g => GuardView(g.id, g.biome, g.x, g.y, g.chasing)).toVector
val eggs = worldEggs.map(e => EggView(e.id, e.name, e.rarity, e.biome, e.x, e.y)).toVector
Snapshot(s.account.coins, s.account.speed, s.account.role, s.account.baseLevel,
s.x, s.y, eventTitle, eventText, announcement, updateNote,
if (now < boostUntil) boostMultiplier else 1.0, boostUntil,
players, guards, eggs, s.account.eggs.toVector, s.account.pets.toVector)
}
private def handle(s: Session, command: String, args: Vector[String]): Boolean = lock.synchronized {
val now = System.currentTimeMillis()
def arg(i: Int): String = args.lift(i).getOrElse("")
def isAdmin: Boolean = s.account.role == "OWNER" || s.account.role == "ADMIN"
def isOwner: Boolean = s.account.role == "OWNER"
command match {
case "SYNC" => writeSnapshot(s.out, buildSnapshot(s))
case "MOVE" =>
val nx = Try(arg(0).toDouble).getOrElse(s.x)
val ny = Try(arg(1).toDouble).getOrElse(s.y)
val dx = nx - s.x; val dy = ny - s.y
val dt = math.max(0.02, math.min(0.5, (now - s.lastMove) / 1000.0))
val maxStep = (1.7 + s.account.speed.toDouble / 180.0) * dt + 0.22
val worldMaxX = biomes.size * 24.0 - 1.0
val safeNx = math.max(0.6, math.min(worldMaxX, nx))
if (math.hypot(dx, dy) <= maxStep && ny >= 1.0 && ny <= 19.0) {
if (canEnter(s.account, safeNx)) s.x = safeNx
else s.x = math.max(0.6, math.min(safeNx, (biomeIndex(safeNx) * 24.0) - 0.4))
s.y = ny
}
s.lastMove = now
updateGuardAI(s, now)
case "TRAIN" =>
if (now - s.lastTrain >= 800L) {
if (isAdmin) s.account.speed = 100000000
else s.account.speed = math.min(100000, s.account.speed + 2)
s.lastTrain = now
announcement = s.account.username + " trained on the treadmill! Speed +2."
saveAccounts()
}
case "UPGRADE_SPEED" =>
val cost = 100L + s.account.speed.toLong * 8L
if (s.account.coins >= cost) {
s.account.coins -= cost
if (isAdmin) s.account.speed = 100000000
else s.account.speed = math.min(100000, s.account.speed + 20)
announcement = s.account.username + " upgraded Speed!"
saveAccounts()
}
case "UPGRADE_BASE" =>
val cost = 250L * s.account.baseLevel
if (s.account.coins >= cost && s.account.baseLevel < 20) {
s.account.coins -= cost
// baseLevel is mutable in Account
s.account.baseLevel += 1
announcement = s.account.username + " upgraded their base!"
saveAccounts()
}
case "COLLECT" =>
val id = arg(0)
worldEggs.indexWhere(_.id == id) match {
case -1 => ()
case ix =>
val e = worldEggs(ix)
if (math.hypot(s.x - e.x, s.y - e.y) <= 2.2 && canEnter(s.account, e.x)) {
worldEggs.remove(ix)
s.account.eggs += EggItem(e.id, e.name, e.rarity, e.biome, now + 12000L)
if (s.guardLootIds.isEmpty) s.guardLootStartedAt = now
s.guardLootIds += e.id
alertGuardsInBiome(s, e.biome)
announcement = "GUARDS ALERTED! " + s.account.username + " grabbed a " + e.rarity + " egg - RUN!"
spawnEgg(e.biome)
saveAccounts()
}
}
case "HATCH" =>
val id = arg(0)
val ix = s.account.eggs.indexWhere(_.id == id)
if (ix >= 0 && now >= s.account.eggs(ix).hatchAt) {
val e = s.account.eggs.remove(ix)
s.guardLootIds -= e.id
if (s.guardLootIds.isEmpty) s.guards.foreach(g => g.chasing = false)
val mult = e.rarity match {
case "Celestial" => 12L
case "Secret" => 8L
case "Mythic" => 5L
case "Legendary" => 3L
case "Epic" => 2L
case _ => 1L
}
val income = eggDefs.find(_.name == e.name).map(_.income).getOrElse(3L) * mult
val petName = e.name.stripSuffix(" Egg") + "ling"
s.account.pets += PetView(petName, e.rarity, income)
announcement = s.account.username + " hatched " + petName + "!"
saveAccounts()
}
case "STEAL_PLAYER" =>
val targetName = arg(0)
sessions.get(norm(targetName)).filterNot(_.account.username == s.account.username).foreach { target =>
if (nearestBase(s.x, s.y, target.account) && target.account.eggs.nonEmpty) {
val stolen = target.account.eggs.remove(target.account.eggs.size - 1)
s.account.eggs += stolen.copy(hatchAt = math.max(stolen.hatchAt, now + 6000L))
if (s.guardLootIds.isEmpty) s.guardLootStartedAt = now
s.guardLootIds += stolen.id
alertGuardsInBiome(s, biomes(biomeIndex(target.account.baseX)).name)
announcement = "BASE GUARDS ALERTED! " + s.account.username + " stole an egg - ESCAPE!"
saveAccounts()
}
}
case "BUY_BOOST" =>
val cost = 500L
if (s.account.coins >= cost) {
s.account.coins -= cost
// A short global boost can be purchased by any player for the whole server.
boostMultiplier = 2.0; boostUntil = now + 60000L
announcement = s.account.username + " activated a 2x server boost for 60 seconds!"
saveAccounts()
}
case "ANNOUNCE" if isAdmin =>
announcement = arg(0).take(220)
case "EVENT" if isAdmin =>
eventTitle = arg(0).take(60)
eventText = arg(1).take(220)
announcement = "EVENT LIVE: " + eventTitle + " ? " + eventText
case "SPAWN_EGG" if isAdmin =>
val biome = arg(0); val rarity = arg(1)
if (biomes.exists(_.name.equalsIgnoreCase(biome))) {
spawnEgg(biome, rarity.take(24))
announcement = "ADMIN SPAWNED A " + rarity.toUpperCase + " EGG IN " + biome.toUpperCase + "!"
}
case "GRANT_COINS" if isOwner =>
val target = accounts.get(norm(arg(0)))
val amount = Try(arg(1).toLong).getOrElse(0L)
if (target.nonEmpty && amount > 0 && amount <= 1000000000L) {
target.get.coins = math.min(Long.MaxValue / 4, target.get.coins + amount)
announcement = s.account.username + " sent " + amount + " coins to " + target.get.username + "."
saveAccounts()
}
case "PROMOTE" if isOwner =>
accounts.get(norm(arg(0))).filterNot(_.username.equalsIgnoreCase("Yadnesh")).foreach { target =>
target.role = if (arg(1).equalsIgnoreCase("ADMIN")) "ADMIN" else "PLAYER"
announcement = target.username + " role changed to " + target.role + "."
saveAccounts()
}
case "BOOST" if isAdmin =>
val mult = Try(arg(0).toDouble).getOrElse(1.0)
val seconds = Try(arg(1).toInt).getOrElse(60).max(5).min(3600)
boostMultiplier = math.max(1.0, math.min(10.0, mult))
boostUntil = now + seconds * 1000L
announcement = "ADMIN EVENT BOOST: " + boostMultiplier + "x for " + seconds + " seconds!"
case "UPDATE_NOTE" if isOwner =>
updateNote = (arg(0) + " ? " + arg(1)).take(240)
announcement = "GAME UPDATE: " + updateNote
case "QUIT" => return false
case _ => () // Unknown or unauthorized commands are ignored server-side.
}
true
}
private def authenticate(in: DataInputStream, out: DataOutputStream): Account = {
val (cmd, args) = GameProtocol.unpack(in.readUTF())
def a(i: Int): String = args.lift(i).getOrElse("")
val name = a(0).trim
val pass = a(1)
val account = lock.synchronized {
cmd match {
case "REGISTER" =>
if (!safeName(name) || name.equalsIgnoreCase("Yadnesh") || pass.length < 8 || accounts.contains(norm(name))) {
out.writeUTF("ERROR|Use a new username and a password of at least 8 characters. Yadnesh is reserved for the owner.")
out.flush(); null
} else {
val salt = randomBytes(16)
val (bx, by) = freshBase(name)
val acc = new Account(name, salt, hashPassword(pass, salt), 250L, 1, "PLAYER", 1, bx, by)
accounts(norm(name)) = acc; saveAccounts(); acc
}
case "OWNER_LOGIN" =>
val supplied = pass.getBytes(java.nio.charset.StandardCharsets.UTF_8)
val expected = ownerBootstrapPassword.getBytes(java.nio.charset.StandardCharsets.UTF_8)
if (!name.equalsIgnoreCase("Yadnesh") ||
!java.security.MessageDigest.isEqual(supplied, expected)) {
out.writeUTF("ERROR|Admin login failed. Use the Yadnesh Owner account credentials.")
out.flush(); null
} else {
accounts.get(norm("Yadnesh")) match {
case Some(existing) if existing.role == "OWNER" =>
var ownerChanged = false
if (!verifyPassword(existing, pass)) {
// Migrate an older Owner profile to the configured Admin Login.
val newSalt = randomBytes(16)
existing.salt = newSalt
existing.hash = hashPassword(pass, newSalt)
ownerChanged = true
println("Existing Yadnesh Owner profile migrated to the configured Admin Login.")
}
// Always restore the creator's requested 100-million speed on Admin login.
if (existing.speed != 100000000) {
existing.speed = 100000000
ownerChanged = true
}
if (ownerChanged) saveAccounts()
existing
case Some(_) =>
out.writeUTF("ERROR|The reserved Yadnesh account is not an Owner account.")
out.flush(); null
case None if accounts.values.exists(_.role == "OWNER") =>
out.writeUTF("ERROR|An Owner account already exists on this server.")
out.flush(); null
case None =>
val salt = randomBytes(16)
val (bx, by) = freshBase("Yadnesh")
val acc = new Account("Yadnesh", salt, hashPassword(pass, salt),
1000000L, 100000000, "OWNER", 10, bx, by)
accounts(norm("Yadnesh")) = acc
saveAccounts()
println("Yadnesh Owner profile created and authenticated.")
acc
}
}
case "LOGIN" =>
accounts.get(norm(name)).filter(a => verifyPassword(a, pass)).orElse {
out.writeUTF("ERROR|Login failed. Check your username and password.")
out.flush(); None
}.orNull
case _ =>
out.writeUTF("ERROR|Unknown login request."); out.flush(); null
}
}
if (account != null) {
val availability = lock.synchronized {
if (sessions.contains(norm(account.username))) "already"
else if (sessions.size >= MaxPlayers) "full"
else "ok"
}
if (availability != "ok") {
val reason = if (availability == "full") "This server is full. Maximum 10 players are allowed."
else "This account is already online on this server."
out.writeUTF("ERROR|" + reason)
out.flush()
null
} else {
out.writeUTF("OK")
out.writeLong(account.coins); out.writeInt(account.speed); out.writeUTF(account.role)
out.flush()
account
}
} else null
}
private def handleSocket(socket: Socket): Unit = {
var session: Session = null
try {
socket.setTcpNoDelay(true)
socket.setSoTimeout(0)
val in = new DataInputStream(new BufferedInputStream(socket.getInputStream))
val out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream))
val account = authenticate(in, out)
if (account == null) return
session = lock.synchronized {
val s = new Session(socket, in, out, account, account.baseX, account.baseY)
initialiseGuards(s)
sessions(norm(account.username)) = s
s
}
println("Connected: " + account.username)
var connected = true
while (connected && running && !socket.isClosed) {
val line = in.readUTF()
val (cmd, args) = GameProtocol.unpack(line)
connected = handle(session, cmd, args)
}
} catch {
case _: java.io.EOFException => ()
case e: Exception => println("Client disconnected: " + Option(e.getMessage).getOrElse(e.getClass.getSimpleName))
} finally {
if (session != null) lock.synchronized {
sessions.get(norm(session.account.username)).filter(_ eq session).foreach(_ => sessions.remove(norm(session.account.username)))
saveAccounts()
}
try socket.close() catch { case _: Throwable => () }
}
}
def main(args: Array[String]): Unit = {
dataDir.mkdirs()
loadAccounts()
seedWorld()
if (!accounts.values.exists(_.role == "OWNER"))
println("First run: open the client, choose ADMIN LOGIN, and sign in as Yadnesh.")
val server = GameProtocol.serverSocket(port)
GameProtocol.startDiscoveryResponder(server.getLocalPort)
println("Steal An Egg server listening on port " + server.getLocalPort)
println("LAN discovery listening on UDP port " + GameProtocol.DiscoveryPort)
println("TLS enabled: " + java.lang.Boolean.getBoolean("stealegg.tls"))
println("Account data: " + accountFile.getAbsolutePath)
val incomeThread = new Thread(() => {
while (running) {
try {
Thread.sleep(1000L)
lock.synchronized {
val now = System.currentTimeMillis()
val mult = if (now < boostUntil) boostMultiplier else 1.0
sessions.values.foreach { s =>
val income = s.account.pets.map(_.income).sum
if (income > 0) s.account.coins = math.min(Long.MaxValue / 4,
s.account.coins + math.max(1L, (income * mult).toLong))
}
if (now % 15000L < 1000L) saveAccounts()
}
} catch { case _: InterruptedException => running = false; case e: Exception => println("Income loop: " + e.getMessage) }
}
}, "steal-egg-income")
incomeThread.setDaemon(true); incomeThread.start()
val acceptThread = new Thread(() => {
while (running) {
try {
val s = server.accept()
val t = new Thread(() => handleSocket(s), "egg-client-" + s.getRemoteSocketAddress)
t.setDaemon(true); t.start()
} catch { case e: Exception => if (running) println("Accept error: " + e.getMessage) }
}
}, "steal-egg-accept")
acceptThread.setDaemon(false); acceptThread.start()
println("Server ready. Press Ctrl+C to stop.")
}
}
import java.awt._
import java.awt.event._
import java.io._
import java.net.Socket
import java.util.concurrent.LinkedBlockingQueue
import javax.swing._
import scala.collection.mutable
import scala.util.Try
import GameProtocol._
/**
* STEAL AN EGG - YADNESH EDITION
* Perspective 3D polygon-rendered multiplayer egg-collecting game client.
* Run: sbt "runMain StealAnEggClient"
*/
object StealAnEggClient {
private val bg = new Color(10, 15, 30)
private val panelBg = new Color(22, 31, 54)
private val cyan = new Color(55, 230, 220)
private val gold = new Color(255, 205, 80)
private val white = new Color(240, 245, 255)
private val fontName = "Arial"
private case class Biome(name: String, gate: Int, color1: Color, color2: Color)
private val biomes = Vector(
Biome("Green Meadow", 0, new Color(91, 174, 92), new Color(54, 132, 73)),
Biome("Whispering Forest", 5, new Color(47, 128, 75), new Color(28, 83, 55)),
Biome("Crystal Lake", 10, new Color(74, 178, 205), new Color(37, 119, 170)),
Biome("Golden Desert", 15, new Color(222, 183, 103), new Color(176, 126, 62)),
Biome("Mystic Jungle", 20, new Color(41, 142, 81), new Color(20, 93, 55)),
Biome("Savannah", 30, new Color(191, 166, 91), new Color(143, 119, 61)),
Biome("Sakura Valley", 40, new Color(231, 153, 190), new Color(173, 104, 151)),
Biome("Frost Peaks", 50, new Color(204, 232, 247), new Color(135, 188, 219)),
Biome("Ice Cavern", 60, new Color(119, 204, 235), new Color(53, 125, 177)),
Biome("Candy Fields", 70, new Color(248, 155, 190), new Color(126, 190, 226)),
Biome("Mushroom Woods", 85, new Color(147, 130, 99), new Color(77, 113, 73)),
Biome("Coral Reef", 100, new Color(73, 180, 177), new Color(37, 119, 148)),
Biome("Deep Ocean", 120, new Color(40, 93, 157), new Color(17, 48, 101)),
Biome("Volcanic Crater", 140, new Color(159, 73, 54), new Color(88, 40, 43)),
Biome("Prehistoric Wilds", 165, new Color(139, 151, 79), new Color(83, 102, 48)),
Biome("Sky Islands", 190, new Color(117, 177, 204), new Color(81, 127, 179)),
Biome("Cloud Kingdom", 220, new Color(214, 227, 244), new Color(147, 177, 217)),
Biome("Crystal Caverns", 250, new Color(127, 103, 194), new Color(75, 63, 147)),
Biome("Cyber City", 285, new Color(47, 101, 133), new Color(27, 57, 105)),
Biome("Cosmic Realm", 320, new Color(101, 75, 168), new Color(49, 37, 104)),
Biome("Void Dimension", 360, new Color(59, 42, 82), new Color(26, 19, 46))
)
private var frame: JFrame = _
private var connection: ClientConnection = _
private var currentUsername = ""
private var currentSnapshot: Snapshot = Snapshot(0, 1, "PLAYER", 1, 3.5, 3.5,
"", "", "", "", 1.0, 0L, Vector.empty, Vector.empty,
Vector.empty, Vector.empty, Vector.empty)
private var gamePanel: WorldPanel = _
private var statusLabel: JLabel = _
// Admin Login starts a local host automatically when this PC is the host.
// Remote hosts are never started locally.
private val localServerStartLock = new Object
private def isLoopbackHost(host: String): Boolean = {
val h = Option(host).getOrElse("").trim.toLowerCase
h == "127.0.0.1" || h == "localhost" || h == "::1"
}
private def canReachTcp(host: String, port: Int, timeoutMs: Int = 450): Boolean = {
val socket = new Socket()
try {
socket.connect(new java.net.InetSocketAddress(host, port), timeoutMs)
true
} catch { case _: Exception => false }
finally { try socket.close() catch { case _: Throwable => () } }
}
private def ensureLocalGameServer(host: String, port: Int): Unit = {
if (!isLoopbackHost(host))
throw new IOException("Enter the host PC IP or use FIND LAN SERVER to connect to a remote host.")
if (canReachTcp(host, port)) return
localServerStartLock.synchronized {
if (!canReachTcp(host, port)) {
System.setProperty("stealegg.tls", "false")
System.setProperty("stealegg.port", port.toString)
try StealAnEggServer.main(Array.empty[String])
catch { case _: java.net.BindException => () }
val deadline = System.nanoTime() + 7000L * 1000000L
var ready = canReachTcp(host, port)
while (!ready && System.nanoTime() < deadline) {
Thread.sleep(150L)
ready = canReachTcp(host, port)
}
if (!ready)
throw new IOException("Local server did not start on port " + port + ". Check whether another app is using this port, then restart the game.")
}
}
}
private var profileStatsLabel: JLabel = _
private def button(text: String): JButton = {
val b = new JButton(text)
b.setFont(new Font(fontName, Font.BOLD, 13))
b.setForeground(white)
b.setBackground(new Color(34, 52, 82))
b.setFocusPainted(false)
b.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(65, 205, 210), 1),
BorderFactory.createEmptyBorder(9, 13, 9, 13)))
b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
b
}
private def label(text: String, size: Int, color: Color): JLabel = {
val l = new JLabel(text)
l.setFont(new Font(fontName, Font.BOLD, size)); l.setForeground(color); l
}
private def panel(layout: LayoutManager): JPanel = {
val p = new JPanel(layout); p.setOpaque(false); p
}
private def showMessage(text: String, title: String = "STEAL AN EGG"): Unit =
JOptionPane.showMessageDialog(frame, text, title, JOptionPane.INFORMATION_MESSAGE)
private def showError(text: String): Unit =
JOptionPane.showMessageDialog(frame, text, "Connection error", JOptionPane.ERROR_MESSAGE)
private def screen(p: JPanel, w: Int, h: Int): Unit = {
frame.setContentPane(p); frame.setSize(w, h); frame.setLocationRelativeTo(null)
frame.revalidate(); frame.repaint(); frame.setVisible(true)
}
private class ClientConnection(val socket: Socket, val in: DataInputStream,
val out: DataOutputStream) {
private val queue = new LinkedBlockingQueue[String]()
@volatile private var alive = true
@volatile var latest: Snapshot = currentSnapshot
def send(cmd: String, args: String*): Unit = {
if (alive) queue.offer(GameProtocol.pack(cmd, args: _*))
}
def close(): Unit = {
alive = false
queue.offer(GameProtocol.pack("QUIT"))
try socket.close() catch { case _: Throwable => () }
}
def start(onState: Snapshot => Unit): Unit = {
val t = new Thread(() => {
try {
while (alive && !socket.isClosed) {
var item = queue.poll(90, java.util.concurrent.TimeUnit.MILLISECONDS)
while (item != null) {
out.writeUTF(item)
item = queue.poll()
}
out.writeUTF(GameProtocol.pack("SYNC"))
out.flush()
val state = GameProtocol.readSnapshot(in)
latest = state
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = onState(state)
})
}
} catch {
case _: InterruptedException => Thread.currentThread().interrupt()
case e: Exception =>
alive = false
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
if (frame != null && frame.isDisplayable)
statusLabel.setText("Connection lost: " + Option(e.getMessage).getOrElse("network error"))
}
})
}
}, "steal-egg-network")
t.setDaemon(true); t.start()
}
}
private def connect(host: String, port: Int, mode: String, username: String,
password: String, useTls: Boolean): Unit = {
if (useTls) System.setProperty("stealegg.tls", "true")
else System.setProperty("stealegg.tls", "false")
val worker = new SwingWorker[ClientConnection, Unit]() {
override def doInBackground(): ClientConnection = {
val socket = GameProtocol.clientSocket(host, port)
socket.setTcpNoDelay(true)
val in = new DataInputStream(new BufferedInputStream(socket.getInputStream))
val out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream))
val request = mode match {
case "Create account" => GameProtocol.pack("REGISTER", username, password)
case "Admin Login" => GameProtocol.pack("OWNER_LOGIN", username, password)
case _ => GameProtocol.pack("LOGIN", username, password)
}
out.writeUTF(request); out.flush()
val answer = in.readUTF()
if (answer != "OK") {
socket.close()
val parts = answer.split("\\|", -1)
val msg = if (parts.length > 1) parts.drop(1).mkString("|") else answer
throw new IOException(if (msg.startsWith("ERROR|")) msg else msg)
}
// Initial account summary; subsequent authoritative state arrives with SYNC.
val coins = in.readLong(); val speed = in.readInt(); val role = in.readUTF()
val c = new ClientConnection(socket, in, out)
c.latest = currentSnapshot.copy(coins = coins, speed = speed, role = role)
c
}
override def done(): Unit = {
try {
val established = get()
if (mode == "Admin Login" && !established.latest.role.equalsIgnoreCase("OWNER")) {
established.close()
throw new IOException("This account does not have Yadnesh OWNER access.")
}
connection = established
currentUsername = if (mode == "Admin Login") "Yadnesh" else username
currentSnapshot = connection.latest
connection.start(onSnapshot)
if (mode == "Admin Login") showAdminLobby()
else showProfileHome()
} catch {
case e: Exception =>
if (connection != null && mode == "Admin Login") {
connection.close()
connection = null
}
showError(Option(e.getCause).map(_.getMessage).getOrElse(e.getMessage))
showLogin()
}
}
}
worker.execute()
}
private def showLogin(): Unit = {
profileStatsLabel = null
val root = new JPanel(new BorderLayout(14, 14))
root.setBackground(bg)
root.setBorder(BorderFactory.createEmptyBorder(22, 28, 22, 28))
val title = label("STEAL AN EGG", 34, gold)
title.setHorizontalAlignment(SwingConstants.CENTER)
val sub = label("YADNESH EDITION ? 3D WORLD ? EGGS ? PETS ? BIOMES", 13, cyan)
sub.setHorizontalAlignment(SwingConstants.CENTER)
val north = panel(new GridLayout(2, 1, 4, 4))
north.add(title); north.add(sub)
root.add(north, BorderLayout.NORTH)
val form = new JPanel(new GridLayout(5, 2, 10, 10))
form.setOpaque(false)
val host = new JTextField("127.0.0.1")
val port = new JTextField(GameProtocol.DefaultPort.toString)
val user = new JTextField()
val pass = new JPasswordField()
val tls = new JCheckBox("Use TLS for hosted Internet server", false)
tls.setOpaque(false); tls.setForeground(white)
form.add(label("SERVER HOST / IP", 13, white)); form.add(host)
form.add(label("SERVER PORT (AUTO)", 13, white)); form.add(port)
form.add(label("PLAYER NAME", 13, white)); form.add(user)
form.add(label("PASSWORD", 13, white)); form.add(pass)
form.add(label("SECURE CONNECTION", 13, white)); form.add(tls)
root.add(form, BorderLayout.CENTER)
val footer = panel(new BorderLayout(8, 8))
val note = new JTextArea(
"Create an account once and your profile is saved on the game server. " +
"For same-Wi-Fi play, use FIND LAN SERVER. Different Wi-Fi requires an online hosted server and TLS. " +
"The automatic port is " + GameProtocol.DefaultPort + "."
)
note.setLineWrap(true); note.setWrapStyleWord(true); note.setEditable(false)
note.setForeground(new Color(195, 207, 226)); note.setBackground(panelBg)
footer.add(note, BorderLayout.CENTER)
val buttons = new JPanel(new GridLayout(2, 3, 8, 8))
buttons.setOpaque(false)
val login = button("LOGIN")
val create = button("CREATE ACCOUNT")
val adminLogin = button("ADMIN LOGIN")
val findServer = button("FIND LAN SERVER")
val startServer = button("START LOCAL SERVER")
val help = button("NETWORK HELP")
def begin(action: String): Unit = {
val h = host.getText.trim
val p = Try(port.getText.trim.toInt).getOrElse(0)
val n = user.getText.trim
val pw = new String(pass.getPassword)
if (h.isEmpty || p < 1 || p > 65535 || n.isEmpty || pw.isEmpty) {
showError("Enter a server host, valid port, username and password.")
} else if (action == "Create account" && pw.length < 8) {
showError("Use a password with at least 8 characters.")
} else {
login.setEnabled(false); create.setEnabled(false)
login.setText("CONNECTING...")
connect(h, p, action, n, pw, tls.isSelected)
}
}
login.addActionListener(_ => begin("Login"))
create.addActionListener(_ => begin("Create account"))
adminLogin.addActionListener(_ => showAdminPasswordLogin(
host.getText.trim,
Try(port.getText.trim.toInt).getOrElse(GameProtocol.DefaultPort),
tls.isSelected
))
findServer.addActionListener(_ => {
findServer.setEnabled(false); findServer.setText("SEARCHING...")
val worker = new SwingWorker[Option[(String, Int)], Unit]() {
override def doInBackground(): Option[(String, Int)] = GameProtocol.discoverServer(2200)
override def done(): Unit = {
findServer.setEnabled(true); findServer.setText("FIND LAN SERVER")
try {
get() match {
case Some((ip, discoveredPort)) =>
host.setText(ip); port.setText(discoveredPort.toString); tls.setSelected(false)
showMessage("Game server found!\nIP: " + ip + "\nPort: " + discoveredPort, "SERVER FOUND")
case None =>
showMessage("No server found on this Wi-Fi. Start the server on the host PC, allow it through Windows Firewall on Private networks, then try again. For a hosted Internet server, enter its hostname manually.", "SERVER NOT FOUND")
}
} catch {
case ex: Exception => showError("LAN discovery failed: " + Option(ex.getCause).map(_.getMessage).getOrElse(ex.getMessage))
}
}
}
worker.execute()
})
startServer.addActionListener(_ => {
startServer.setEnabled(false); startServer.setText("SERVER STARTING...")
val t = new Thread(() => {
try {
ensureLocalGameServer("127.0.0.1", GameProtocol.DefaultPort)
SwingUtilities.invokeLater(() => {
host.setText("127.0.0.1")
port.setText(GameProtocol.DefaultPort.toString)
startServer.setText("SERVER RUNNING")
showMessage(
"Local server started.\nThis PC: 127.0.0.1:" + GameProtocol.DefaultPort +
"\nSame Wi-Fi friends can click FIND LAN SERVER.\nLAN IP: " +
GameProtocol.localIPv4Address() + ":" + GameProtocol.DefaultPort +
"\nAdmin: choose ADMIN LOGIN and use the Yadnesh Owner credentials.",
"LOCAL SERVER"
)
})
} catch {
case ex: Exception => SwingUtilities.invokeLater(() => {
startServer.setEnabled(true); startServer.setText("START LOCAL SERVER")
showError("Server start failed: " + ex.getMessage)
})
}
}, "stealegg-local-server-launch")
t.setDaemon(false); t.start()
})
help.addActionListener(_ => showMessage(
"1. On the host PC, ADMIN LOGIN can start the local server automatically.\n" +
"2. On another PC on the same Wi-Fi, click FIND LAN SERVER.\n" +
"3. The IP and port are filled automatically if UDP broadcast is allowed.\n" +
"4. Create an account, or use ADMIN LOGIN for the Yadnesh Owner profile.\n" +
"5. GAME ON opens the 3D world after login.\n\n" +
"Different Wi-Fi networks need a publicly hosted server, TLS certificate and an Internet hostname. LAN auto-discovery cannot cross the Internet."
, "NETWORK HELP"))
buttons.add(login); buttons.add(create); buttons.add(adminLogin)
buttons.add(findServer); buttons.add(startServer); buttons.add(help)
footer.add(buttons, BorderLayout.SOUTH)
root.add(footer, BorderLayout.SOUTH)
screen(root, 920, 660)
}
/** Password-only owner entry. The username is always fixed to Yadnesh. */
private def showAdminPasswordLogin(fallbackHost: String, fallbackPort: Int,
useTls: Boolean): Unit = {
val passwordField = new JPasswordField(22)
val content = new JPanel(new GridLayout(2, 1, 8, 8))
content.setOpaque(false)
content.add(label("ENTER OWNER PASSWORD", 14, white))
content.add(passwordField)
val choice = JOptionPane.showConfirmDialog(
frame, content, "YADNESH ADMIN LOGIN",
JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE
)
if (choice != JOptionPane.OK_OPTION) return
val password = new String(passwordField.getPassword)
if (password.isEmpty) {
showError("Enter the Admin password.")
return
}
val safeHost = Option(fallbackHost).map(_.trim).filter(_.nonEmpty).getOrElse("127.0.0.1")
val safePort = if (fallbackPort >= 1 && fallbackPort <= 65535)
fallbackPort else GameProtocol.DefaultPort
val worker = new SwingWorker[Option[(String, Int)], Unit]() {
override def doInBackground(): Option[(String, Int)] = {
val discovered = if (useTls) None else GameProtocol.discoverServer(1600)
val target = discovered.getOrElse((safeHost, safePort))
if (discovered.isEmpty && isLoopbackHost(target._1) && !useTls)
ensureLocalGameServer(target._1, target._2)
discovered.orElse(Some(target))
}
override def done(): Unit = {
try {
val target = get().getOrElse((safeHost, safePort))
connect(target._1, target._2, "Admin Login", "Yadnesh", password, useTls)
} catch {
case e: Exception => showError("Could not find or connect to the game server: " +
Option(e.getCause).map(_.getMessage).getOrElse(e.getMessage))
}
}
}
worker.execute()
}
/** Shown immediately after a successful password-only Yadnesh Owner login. */
private def showAdminLobby(): Unit = {
profileStatsLabel = null
val s = if (connection != null) connection.latest else currentSnapshot
val root = new JPanel(new BorderLayout(16, 16))
root.setBackground(bg)
root.setBorder(BorderFactory.createEmptyBorder(24, 34, 24, 34))
val heading = label("YADNESH OWNER LOBBY", 32, gold)
heading.setHorizontalAlignment(SwingConstants.CENTER)
val sub = label("OWNER ACCESS VERIFIED ? STEAL AN EGG ? 21 BIOMES", 14, cyan)
sub.setHorizontalAlignment(SwingConstants.CENTER)
val north = panel(new GridLayout(2, 1, 4, 5))
north.add(heading); north.add(sub)
root.add(north, BorderLayout.NORTH)
val center = new JPanel(new GridLayout(0, 1, 12, 12))
center.setOpaque(false)
center.add(label("WELCOME BACK, YADNESH!", 27, white))
center.add(label("ROLE: OWNER / GAME CREATOR", 19, gold))
profileStatsLabel = label(
"COINS: " + fmt(s.coins) + " SPEED: " + s.speed +
" BASE LEVEL: " + s.baseLevel + " EGGS: " + s.eggs.size +
" PETS: " + s.pets.size,
16, white
)
center.add(profileStatsLabel)
center.add(label("SERVER WORLD: 21 BIOMES ? MAXIMUM PLAYERS: 10", 14,
new Color(185, 203, 224)))
root.add(center, BorderLayout.CENTER)
val actions = panel(new GridLayout(1, 4, 10, 10))
val gameOn = button("? GAME ON")
val controls = button("ADMIN CONTROL PANEL")
val profile = button("MY PROFILE")
val logout = button("LOG OUT")
gameOn.setFont(new Font(fontName, Font.BOLD, 18))
gameOn.addActionListener(_ => showGame())
controls.addActionListener(_ => showAdminPanel())
profile.addActionListener(_ => showProfileHome())
logout.addActionListener(_ => {
if (connection != null) connection.close()
connection = null
gamePanel = null
profileStatsLabel = null
currentUsername = ""
showLogin()
})
actions.add(gameOn); actions.add(controls); actions.add(profile); actions.add(logout)
root.add(actions, BorderLayout.SOUTH)
screen(root, 960, 620)
}
private def showProfileHome(): Unit = {
profileStatsLabel = null
val s = if (connection != null) connection.latest else currentSnapshot
val root = new JPanel(new BorderLayout(14, 14))
root.setBackground(bg)
root.setBorder(BorderFactory.createEmptyBorder(24, 36, 24, 36))
val title = label("PLAYER PROFILE", 30, gold)
title.setHorizontalAlignment(SwingConstants.CENTER)
val sub = label("STEAL AN EGG ? YADNESH EDITION", 14, cyan)
sub.setHorizontalAlignment(SwingConstants.CENTER)
val north = panel(new GridLayout(2, 1, 4, 4)); north.add(title); north.add(sub)
root.add(north, BorderLayout.NORTH)
val center = new JPanel(new GridLayout(0, 1, 10, 10))
center.setOpaque(false)
val roleText = if (s.role == "OWNER") "OWNER / GAME CREATOR" else if (s.role == "ADMIN") "ADMIN" else "PLAYER"
center.add(label("WELCOME, " + currentUsername.toUpperCase(java.util.Locale.ROOT), 25, white))
center.add(label("ROLE: " + roleText, 18, if (s.role == "OWNER") gold else cyan))
profileStatsLabel = label(
"COINS: " + fmt(s.coins) + " SPEED: " + s.speed +
" BASE LEVEL: " + s.baseLevel + " EGGS: " + s.eggs.size + " PETS: " + s.pets.size,
15, white
)
center.add(profileStatsLabel)
center.add(label("WORLD: 21 BIOMES ? MAX PLAYERS: 10", 14, new Color(185, 203, 224)))
root.add(center, BorderLayout.CENTER)
val actions = panel(new FlowLayout(FlowLayout.CENTER, 12, 10))
val gameOn = button("? GAME ON")
val admin = button("OWNER / ADMIN PANEL")
val logout = button("LOG OUT")
gameOn.setFont(new Font(fontName, Font.BOLD, 18))
gameOn.addActionListener(_ => showGame())
admin.addActionListener(_ => showAdminPanel())
logout.addActionListener(_ => {
if (connection != null) connection.close()
connection = null; gamePanel = null; profileStatsLabel = null
currentUsername = ""; showLogin()
})
actions.add(gameOn)
if (s.role == "OWNER" || s.role == "ADMIN") actions.add(admin)
actions.add(logout)
root.add(actions, BorderLayout.SOUTH)
screen(root, 900, 610)
}
private def onSnapshot(s: Snapshot): Unit = {
currentSnapshot = s
if (profileStatsLabel != null) {
profileStatsLabel.setText("COINS: " + fmt(s.coins) + " SPEED: " + s.speed +
" BASE LEVEL: " + s.baseLevel + " EGGS: " + s.eggs.size + " PETS: " + s.pets.size)
}
if (gamePanel != null) gamePanel.repaint()
if (statusLabel != null) {
val biome = biomeFor(s.x)
val guardText = if (s.guards.exists(_.chasing)) " GUARDS: CHASING!" else " GUARDS: PATROL"
statusLabel.setText("COINS " + fmt(s.coins) + " SPEED " + s.speed +
" BIOME " + biome.name.toUpperCase + " PETS " + s.pets.size +
" EGGS " + s.eggs.size + guardText)
}
}
private def fmt(n: Long): String = {
if (n >= 1000000000L) f"${n / 1000000000.0}%.1fB"
else if (n >= 1000000L) f"${n / 1000000.0}%.1fM"
else if (n >= 1000L) f"${n / 1000.0}%.1fK"
else n.toString
}
private def biomeFor(x: Double): Biome = biomes(math.max(0, math.min(biomes.size - 1, (x / 24.0).toInt)))
private class WorldPanel extends JPanel {
// Real perspective 3D software renderer: 3D vertices -> camera space -> perspective projection.
// It uses Java2D polygons so it remains dependency-free in a Scala Swing project.
private case class V3(x: Double, y: Double, z: Double)
private case class Tri3(a: V3, b: V3, c: V3, color: Color)
private case class CamV(x: Double, y: Double, z: Double)
private case class DrawPoly(depth: Double, xs: Array[Int], ys: Array[Int], color: Color)
private val keys = mutable.HashSet.empty[Int]
private var px = currentSnapshot.x
private var py = currentSnapshot.y
private var yaw = math.Pi / 2.0 // face east so W moves towards the next biome
private var lastMoveSend = 0L
private var mouseLastX = -1
private var dragLook = false
private val nearPlane = 0.28
private val worldLength = biomes.size * 24.0
private val sceneTriangles: Vector[Tri3] = buildScene()
private val timer = new Timer(33, new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = tick()
})
setFocusable(true)
setBackground(new Color(8, 13, 27))
setFocusTraversalKeysEnabled(false)
addKeyListener(new KeyAdapter {
override def keyPressed(e: KeyEvent): Unit = {
keys += e.getKeyCode
e.getKeyCode match {
case KeyEvent.VK_E => interact()
case KeyEvent.VK_T => if (connection != null) connection.send("TRAIN")
case KeyEvent.VK_I => showInventory()
case KeyEvent.VK_M => showBiomes()
case KeyEvent.VK_ESCAPE => keys.clear()
case _ => ()
}
}
override def keyReleased(e: KeyEvent): Unit = keys -= e.getKeyCode
})
addMouseListener(new MouseAdapter {
override def mousePressed(e: MouseEvent): Unit = {
dragLook = true
mouseLastX = e.getX
requestFocusInWindow()
}
override def mouseReleased(e: MouseEvent): Unit = {
dragLook = false
mouseLastX = -1
}
})
addMouseMotionListener(new MouseMotionAdapter {
override def mouseDragged(e: MouseEvent): Unit = {
if (dragLook && mouseLastX >= 0) {
yaw += (e.getX - mouseLastX) * 0.008
mouseLastX = e.getX
}
}
})
timer.start()
private def clamp(v: Double, lo: Double, hi: Double): Double =
math.max(lo, math.min(hi, v))
private def colorScale(c: Color, f: Double): Color = {
val k = clamp(f, 0.12, 1.30)
new Color(
clamp(c.getRed * k, 0, 255).toInt,
clamp(c.getGreen * k, 0, 255).toInt,
clamp(c.getBlue * k, 0, 255).toInt
)
}
private def addQuad(out: mutable.ArrayBuffer[Tri3], x0: Double, z0: Double,
x1: Double, z1: Double, y: Double, color: Color): Unit = {
val a = V3(x0, y, z0); val b = V3(x1, y, z0)
val c = V3(x1, y, z1); val d = V3(x0, y, z1)
// Winding gives an upward-facing surface.
out += Tri3(a, d, c, color)
out += Tri3(a, c, b, color)
}
private def addBox(out: mutable.ArrayBuffer[Tri3], cx: Double, cy: Double, cz: Double,
sx: Double, sy: Double, sz: Double, color: Color): Unit = {
val x0 = cx - sx / 2.0; val x1 = cx + sx / 2.0
val y0 = cy - sy / 2.0; val y1 = cy + sy / 2.0
val z0 = cz - sz / 2.0; val z1 = cz + sz / 2.0
val v = Array(
V3(x0,y0,z0), V3(x1,y0,z0), V3(x1,y1,z0), V3(x0,y1,z0),
V3(x0,y0,z1), V3(x1,y0,z1), V3(x1,y1,z1), V3(x0,y1,z1)
)
val faces = Array(
Array(0,1,2,3), Array(4,7,6,5), Array(0,4,5,1),
Array(3,2,6,7), Array(0,3,7,4), Array(1,5,6,2)
)
val light = Array(0.78, 1.08, 0.68, 0.92, 0.82, 0.76)
var i = 0
while (i < faces.length) {
val f = faces(i); val col = colorScale(color, light(i))
out += Tri3(v(f(0)), v(f(1)), v(f(2)), col)
out += Tri3(v(f(0)), v(f(2)), v(f(3)), col)
i += 1
}
}
private def addPyramid(out: mutable.ArrayBuffer[Tri3], x: Double, y: Double, z: Double,
width: Double, height: Double, color: Color): Unit = {
val a = V3(x-width/2,y,z-width/2); val b = V3(x+width/2,y,z-width/2)
val c = V3(x+width/2,y,z+width/2); val d = V3(x-width/2,y,z+width/2)
val top = V3(x,y+height,z)
out += Tri3(a,b,top,colorScale(color,0.92))
out += Tri3(b,c,top,colorScale(color,1.08))
out += Tri3(c,d,top,colorScale(color,0.82))
out += Tri3(d,a,top,colorScale(color,1.0))
out += Tri3(a,d,c,colorScale(color,0.58))
out += Tri3(a,c,b,colorScale(color,0.58))
}
private def addSphere(out: mutable.ArrayBuffer[Tri3], cx: Double, cy: Double, cz: Double,
rx: Double, ry: Double, rz: Double, color: Color,
rings: Int = 5, slices: Int = 8): Unit = {
def point(r: Int, s: Int): V3 = {
val theta = math.Pi * r.toDouble / rings
val phi = 2.0 * math.Pi * s.toDouble / slices
V3(cx + rx * math.sin(theta) * math.cos(phi),
cy + ry * math.cos(theta),
cz + rz * math.sin(theta) * math.sin(phi))
}
var r = 0
while (r < rings) {
var s = 0
while (s < slices) {
val p00 = point(r,s); val p01 = point(r,s+1)
val p10 = point(r+1,s); val p11 = point(r+1,s+1)
val col1 = colorScale(color, 0.88 + 0.20 * math.sin((s + 1) * 0.8))
val col2 = colorScale(color, 0.92 + 0.17 * math.cos((s + 2) * 0.7))
if (r != 0) out += Tri3(p00,p10,p01,col1)
if (r != rings - 1) out += Tri3(p01,p10,p11,col2)
s += 1
}
r += 1
}
}
private def buildScene(): Vector[Tri3] = {
val out = mutable.ArrayBuffer.empty[Tri3]
val meadow = new Color(76, 148, 89)
val sceneryPositions = Array((4.0,3.8),(9.0,16.5),(16.0,4.0),(20.0,15.7),(6.0,10.0),(18.0,10.0))
biomes.zipWithIndex.foreach { case (biome, index) =>
val start = index * 24.0
addBox(out, start + 12.0, -0.24, 10.0, 24.0, 0.45, 20.0, biome.color1)
addBox(out, start + 12.0, -0.005, 10.0, 24.0, 0.035, 2.2,
colorScale(biome.color2, 1.10))
// Natural low-poly landmarks, with a distinct visual style in every biome.
sceneryPositions.zipWithIndex.foreach { case ((localX, z), j) =>
val x = start + localX
val offset = ((index * 19 + j * 13) % 7) * 0.16
val main = biome.color2
biome.name match {
case "Golden Desert" =>
addBox(out,x,0.85+offset,z,0.55,1.7,0.55,new Color(42,137,73))
addBox(out,x+0.35,0.83+offset,z,0.40,0.45,0.42,new Color(42,137,73))
addPyramid(out,x-0.7,0.0,z+0.7,1.4,1.25,new Color(204,158,81))
case "Candy Fields" =>
addBox(out,x,0.70,z,0.22,1.4,0.22,Color.WHITE)
addSphere(out,x,1.55,z,0.72,0.62,0.72,
if (j%2==0) new Color(245,105,160) else new Color(110,205,250),4,8)
case "Mushroom Woods" =>
addBox(out,x,0.48,z,0.30,0.95,0.30,new Color(221,205,169))
addSphere(out,x,1.10,z,0.88,0.45,0.82,
if (j%2==0) new Color(216,75,85) else new Color(147,98,205),4,8)
case "Volcanic Crater" =>
addPyramid(out,x,0.0,z,2.2,2.6,new Color(70,48,48))
addSphere(out,x,2.05,z,0.38,0.22,0.38,new Color(255,94,35),3,7)
case "Cyber City" =>
val h = 2.0 + ((index+j)%4)*0.85
addBox(out,x,h/2,z,1.25,h,1.2,new Color(45,66,111))
addBox(out,x,h*0.68,z,1.33,0.10,1.28,new Color(43,226,232))
addBox(out,x+0.55,h*0.33,z+0.62,0.10,0.48,0.08,new Color(255,89,194))
case "Cosmic Realm" | "Void Dimension" =>
addPyramid(out,x,0.0,z,1.3,2.2,main)
addSphere(out,x,2.9+offset,z,0.80,0.80,0.80,
if (biome.name=="Void Dimension") new Color(90,47,142) else new Color(93,119,235),4,8)
case "Sky Islands" | "Cloud Kingdom" =>
addBox(out,x,0.45,z,1.3,0.9,1.3,new Color(115,126,145))
addSphere(out,x,2.5+offset,z,1.35,0.65,1.0,new Color(225,235,247),4,8)
case "Crystal Lake" | "Ice Cavern" | "Crystal Caverns" =>
addPyramid(out,x,0.0,z,0.72,2.0+offset,new Color(79,196,226))
addPyramid(out,x+0.65,0.0,z+0.4,0.48,1.35,new Color(153,230,248))
case "Coral Reef" | "Deep Ocean" =>
addBox(out,x,0.95,z,0.24,1.9,0.24,new Color(230,105,139))
addBox(out,x-0.35,1.25,z,0.75,0.18,0.22,new Color(237,145,118))
addSphere(out,x+0.5,0.8,z+0.25,0.42,0.52,0.38,new Color(75,183,173),4,7)
case _ =>
val trunk = if (biome.name.contains("Snow") || biome.name.contains("Frost"))
new Color(105,100,92) else new Color(104,71,43)
addBox(out,x,0.9,z,0.36,1.8,0.36,trunk)
val canopy = biome.name match {
case "Sakura Valley" => new Color(246,139,190)
case "Savannah" => new Color(184,157,78)
case "Frost Peaks" => new Color(208,233,247)
case "Prehistoric Wilds" => new Color(71,139,71)
case "Mystic Jungle" => new Color(35,106,66)
case "Whispering Forest" => new Color(35,93,57)
case _ => new Color(69,158,86)
}
addSphere(out,x,2.15+offset,z,1.18,1.05,1.18,canopy,4,8)
if (biome.name == "Frost Peaks")
addPyramid(out,x,0.0,z,1.65,2.1,new Color(190,220,242))
}
if (j % 2 == 0) addSphere(out,x+1.0,0.22,z+0.6,0.18,0.22,0.18,
if (biome.name == "Volcanic Crater") new Color(255,100,38) else biome.color2,3,6)
}
// Animal-guard outpost: timber supports, lookout platform and roof.
addBox(out,start + 3.2,0.92,10.0,0.42,1.84,0.42,new Color(91,73,52))
addBox(out,start + 4.45,0.92,10.0,0.42,1.84,0.42,new Color(91,73,52))
addBox(out,start + 3.82,1.92,10.0,2.15,0.20,2.05,new Color(128,104,68))
addPyramid(out,start + 3.82,2.02,10.0,2.45,1.15,new Color(62,74,60))
addBox(out,start + 3.82,1.42,8.94,0.55,0.48,0.12,new Color(230,190,74))
// Large 3D gate marking the entrance to the biome.
if (index > 0) {
val gx = start + 0.55
addBox(out,gx,1.7,2.0,0.45,3.4,0.45,new Color(42,54,75))
addBox(out,gx,1.7,18.0,0.45,3.4,0.45,new Color(42,54,75))
addBox(out,gx,3.28,10.0,0.48,0.42,16.3,
if (index%2==0) new Color(78,216,229) else new Color(203,119,255))
}
}
out.toVector
}
private def tick(): Unit = {
if (connection == null) return
val oldX = px; val oldY = py
val dt = 0.033
if (keys.contains(KeyEvent.VK_LEFT)) yaw -= 1.9 * dt
if (keys.contains(KeyEvent.VK_RIGHT)) yaw += 1.9 * dt
var forward = 0.0
var strafe = 0.0
if (keys.contains(KeyEvent.VK_W) || keys.contains(KeyEvent.VK_UP)) forward += 1.0
if (keys.contains(KeyEvent.VK_S) || keys.contains(KeyEvent.VK_DOWN)) forward -= 1.0
if (keys.contains(KeyEvent.VK_A)) strafe -= 1.0
if (keys.contains(KeyEvent.VK_D)) strafe += 1.0
val len = math.hypot(forward, strafe)
if (len > 0.0) {
val sprint = keys.contains(KeyEvent.VK_SHIFT)
// Speed stat controls movement. Log scaling keeps 100,000,000 fast
// but still steerable in the 3D world rather than teleporting instantly.
val speedStat = math.max(1, currentSnapshot.speed).toDouble
val speedFactor = math.min(80.0, 1.0 + math.log10(speedStat) * 10.0)
val moveSpeed = (if (sprint) 5.0 else 2.7) * speedFactor
val f = forward / len * moveSpeed * dt
val s = strafe / len * moveSpeed * dt
val dx = math.sin(yaw) * f + math.cos(yaw) * s
val dz = math.cos(yaw) * f - math.sin(yaw) * s
px = clamp(px + dx, 0.6, worldLength - 1.0)
py = clamp(py + dz, 1.2, 18.8)
}
val now = System.currentTimeMillis()
if ((math.abs(px - oldX) + math.abs(py - oldY) > 0.001) && now - lastMoveSend > 80) {
connection.send("MOVE", px.toString, py.toString)
lastMoveSend = now
}
if (len == 0.0) {
px = currentSnapshot.x
py = currentSnapshot.y
}
repaint()
}
private def interact(): Unit = {
if (connection == null) return
val state = currentSnapshot
val nearbyEgg = state.worldEggs.filter(e => math.hypot(e.x - px, e.y - py) < 2.7)
.sortBy(e => math.hypot(e.x - px, e.y - py)).headOption
nearbyEgg match {
case Some(e) => connection.send("COLLECT", e.id)
case None =>
val nearbyPlayer = state.players.filterNot(_.name.equalsIgnoreCase(currentUsername))
.filter(p => math.hypot(p.baseX - px, p.baseY - py) < 3.5).headOption
nearbyPlayer match {
case Some(p) => connection.send("STEAL_PLAYER", p.name)
case None => Toolkit.getDefaultToolkit.beep()
}
}
}
private def normalize(v: V3): V3 = {
val n = math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
if (n < 0.00001) V3(0,0,0) else V3(v.x/n,v.y/n,v.z/n)
}
private def dot(a: V3,b: V3): Double = a.x*b.x+a.y*b.y+a.z*b.z
private def subtract(a: V3,b: V3): V3 = V3(a.x-b.x,a.y-b.y,a.z-b.z)
private def cross(a: V3,b: V3): V3 = V3(
a.y*b.z-a.z*b.y, a.z*b.x-a.x*b.z, a.x*b.y-a.y*b.x
)
private def cameraBasis(w: Int, h: Int): (V3,V3,V3,V3) = {
val target = V3(px, 1.0, py)
val cam = V3(px - math.sin(yaw)*8.2, 6.1, py - math.cos(yaw)*8.2)
val forward = normalize(subtract(target,cam))
val right = normalize(cross(forward,V3(0,1,0)))
val up = normalize(cross(right,forward))
(cam,right,up,forward)
}
private def toCamera(v: V3, cam: V3, right: V3, up: V3, forward: V3): CamV = {
val rel = subtract(v,cam)
CamV(dot(rel,right),dot(rel,up),dot(rel,forward))
}
private def clipNear(poly: scala.collection.immutable.List[CamV]): scala.collection.immutable.List[CamV] = {
if (poly.isEmpty) return scala.collection.immutable.List.empty[CamV]
val out = mutable.ListBuffer.empty[CamV]
var prev = poly.last
var prevInside = prev.z >= nearPlane
poly.foreach { cur =>
val curInside = cur.z >= nearPlane
if (curInside != prevInside) {
val denom = cur.z - prev.z
if (math.abs(denom) > 0.0000001) {
val t = (nearPlane - prev.z) / denom
out += CamV(
prev.x + (cur.x-prev.x)*t,
prev.y + (cur.y-prev.y)*t,
nearPlane
)
}
}
if (curInside) out += cur
prev = cur
prevInside = curInside
}
out.toList
}
private def faceLighting(t: Tri3, depth: Double): Color = {
val u = subtract(t.b,t.a); val v = subtract(t.c,t.a)
val n = normalize(cross(u,v))
val light = normalize(V3(-0.35,0.88,-0.30))
val diffuse = math.max(0.0,dot(n,light))
val fog = clamp(1.0 - depth/105.0,0.34,1.0)
colorScale(t.color,(0.34 + diffuse*0.72)*fog)
}
private def projectTriangle(t: Tri3, basis: (V3,V3,V3,V3), w: Int, h: Int): Option[DrawPoly] = {
val (cam,right,up,forward) = basis
val poly = clipNear(scala.collection.immutable.List(
toCamera(t.a,cam,right,up,forward),
toCamera(t.b,cam,right,up,forward),
toCamera(t.c,cam,right,up,forward)
))
if (poly.size < 3) return None
val focal = math.min(w,h)*0.88
val xs = poly.map(p => clamp(w/2.0 + p.x*focal/p.z,-4000,w+4000).toInt).toArray
val ys = poly.map(p => clamp(h/2.0 - p.y*focal/p.z,-4000,h+4000).toInt).toArray
val depth = poly.map(_.z).sum/poly.size
Some(DrawPoly(depth,xs,ys,faceLighting(t,depth)))
}
private def addEgg(out: mutable.ArrayBuffer[Tri3], x: Double, z: Double, rarity: String): Unit = {
val c = rarity match {
case "Uncommon" => new Color(72,218,125)
case "Rare" => new Color(75,160,255)
case "Epic" => new Color(181,87,255)
case "Legendary" => new Color(255,192,55)
case "Mythic" => new Color(255,79,76)
case "Secret" => new Color(248,76,203)
case "Celestial" => new Color(90,235,255)
case _ => new Color(228,233,242)
}
addSphere(out,x,0.85,z,0.48,0.68,0.43,c,6,9)
addSphere(out,x-0.12,1.03,z-0.23,0.09,0.12,0.06,Color.WHITE,3,6)
}
private def addPlayerModel(out: mutable.ArrayBuffer[Tri3], x: Double, z: Double,
name: String, owner: Boolean): Unit = {
val body = if (owner) new Color(250,185,48) else new Color(53,178,211)
addBox(out,x,0.92,z,0.68,1.15,0.42,body)
addSphere(out,x,1.75,z,0.36,0.38,0.34,new Color(239,194,157),5,8)
addSphere(out,x,1.96,z-0.03,0.38,0.16,0.37,new Color(48,37,31),3,8)
addBox(out,x-0.20,0.27,z,0.22,0.55,0.24,new Color(36,43,66))
addBox(out,x+0.20,0.27,z,0.22,0.55,0.24,new Color(36,43,66))
addBox(out,x-0.46,0.96,z,0.19,0.82,0.20,body)
addBox(out,x+0.46,0.96,z,0.19,0.82,0.20,body)
}
private def addGuardModel(out: mutable.ArrayBuffer[Tri3], x: Double, z: Double,
biome: String, chasing: Boolean): Unit = {
val uniform = biome match {
case "Frost Peaks" | "Ice Cavern" => new Color(75, 103, 126)
case "Volcanic Crater" => new Color(112, 61, 47)
case "Cyber City" => new Color(35, 89, 112)
case "Cosmic Realm" | "Void Dimension" => new Color(76, 65, 112)
case _ => new Color(72, 91, 61)
}
val pants = new Color(42, 48, 55)
val helmet = new Color(35, 47, 43)
val bob = if (chasing) math.sin(System.currentTimeMillis() / 85.0) * 0.075 else 0.0
val swing = if (chasing) math.sin(System.currentTimeMillis() / 95.0) * 0.20 else 0.0
// Boots and legs
addBox(out,x-0.19,0.27,z+swing,0.22,0.55,0.27,pants)
addBox(out,x+0.19,0.27,z-swing,0.22,0.55,0.27,pants)
addBox(out,x-0.19,0.075,z+swing-0.035,0.29,0.14,0.43,new Color(28,31,34))
addBox(out,x+0.19,0.075,z-swing-0.035,0.29,0.14,0.43,new Color(28,31,34))
// Protective vest, shoulders and arms
addBox(out,x,0.98+bob,z,0.76,1.02,0.46,uniform)
addBox(out,x,0.96+bob,z-0.245,0.43,0.54,0.035,new Color(191,163,78))
addBox(out,x-0.48,0.99+bob,z+swing,0.22,0.78,0.25,uniform)
addBox(out,x+0.48,0.99+bob,z-swing,0.22,0.78,0.25,uniform)
addSphere(out,x-0.48,0.55+bob,z+swing,0.14,0.16,0.14,
new Color(224,177,139),3,6)
addSphere(out,x+0.48,0.55+bob,z-swing,0.14,0.16,0.14,
new Color(224,177,139),3,6)
// Neck, face and cap/helmet with a bright guard badge
addBox(out,x,1.58+bob,z,0.22,0.20,0.22,new Color(190,143,112))
addSphere(out,x,1.88+bob,z,0.34,0.36,0.32,new Color(226,185,151),5,8)
addBox(out,x,2.13+bob,z,0.73,0.16,0.54,helmet)
addBox(out,x,2.04+bob,z-0.20,0.88,0.08,0.18,helmet)
addBox(out,x+0.17,1.02+bob,z-0.245,0.12,0.28,0.04,new Color(255,214,81))
if (chasing) {
// Small red shoulder beacon is a clear visual alert, not a weapon.
addSphere(out,x+0.36,1.48+bob,z,0.11,0.11,0.11,new Color(255,70,62),3,6)
}
}
private def addBase(out: mutable.ArrayBuffer[Tri3], x: Double, z: Double, mine: Boolean): Unit = {
val col = if (mine) new Color(48,183,172) else new Color(105,124,158)
addBox(out,x,0.12,z,5.0,0.24,4.8,col)
addBox(out,x,1.0,z,3.0,1.75,2.8,new Color(51,67,95))
addPyramid(out,x,1.88,z,3.5,1.5,new Color(43,52,79))
addBox(out,x,0.73,z-1.43,0.8,1.15,0.08,new Color(237,193,74))
addBox(out,x+2.0,0.18,z,1.1,0.28,2.0,new Color(27,36,51)) // treadmill
addBox(out,x+2.0,0.36,z,1.0,0.08,1.9,new Color(68,220,222))
}
private def makeDrawList(w: Int, h: Int): Vector[DrawPoly] = {
val basis = cameraBasis(w,h)
val all = mutable.ArrayBuffer.empty[Tri3]
// Static world is culled by distance before projecting.
sceneTriangles.foreach { t =>
val cx = (t.a.x+t.b.x+t.c.x)/3.0
val cz = (t.a.z+t.b.z+t.c.z)/3.0
if (math.hypot(cx-px,cz-py) < 44.0) all += t
}
currentSnapshot.worldEggs.foreach { e =>
if (math.hypot(e.x-px,e.y-py) < 38.0) addEgg(all,e.x,e.y,e.rarity)
}
currentSnapshot.players.foreach { p =>
if (math.hypot(p.baseX-px,p.baseY-py) < 42.0)
addBase(all,p.baseX,p.baseY,p.name.equalsIgnoreCase(currentUsername))
if (math.hypot(p.x-px,p.y-py) < 42.0 && !p.name.equalsIgnoreCase(currentUsername))
addPlayerModel(all,p.x,p.y,p.name,p.role=="OWNER")
}
currentSnapshot.guards.foreach { guard =>
if (math.hypot(guard.x-px,guard.y-py) < 46.0)
addGuardModel(all,guard.x,guard.y,guard.biome,guard.chasing)
}
addPlayerModel(all,px,py,"YOU",true)
all.iterator.flatMap(t => projectTriangle(t,basis,w,h)).toVector.sortBy(p => -p.depth)
}
private def drawHud(g: Graphics2D, w: Int, h: Int): Unit = {
val s = currentSnapshot
g.setColor(new Color(5,10,23,210)); g.fillRoundRect(14,14,430,102,16,16)
g.setFont(new Font(fontName,Font.BOLD,17)); g.setColor(gold)
g.drawString("STEAL AN EGG ? 3D ANIMAL GUARD WORLD",28,39)
g.setFont(new Font(fontName,Font.BOLD,13)); g.setColor(white)
g.drawString("COINS "+fmt(s.coins)+" SPEED "+s.speed+" PETS "+s.pets.size,28,62)
g.drawString("BIOME "+biomeFor(px).name.toUpperCase+" EGGS "+s.eggs.size+" PLAYERS "+s.players.size+"/10",28,84)
val chasingCount = s.guards.count(_.chasing)
g.setColor(if (chasingCount > 0) new Color(255,91,78) else new Color(167,220,175))
g.drawString("ANIMAL GUARDS: " + (if (chasingCount > 0) "CHASING YOU x" + chasingCount else "PATROLLING"),28,105)
g.setColor(new Color(5,10,23,190)); g.fillRoundRect(14,h-48,Math.min(w-28,700),34,12,12)
g.setFont(new Font(fontName,Font.BOLD,12)); g.setColor(new Color(224,241,255))
g.drawString("WASD MOVE ? SHIFT SPRINT ? MOUSE DRAG / ARROWS LOOK ? E COLLECT / STEAL ? T TRAIN ? RUN TO YOUR BASE TO ESCAPE GUARDS",25,h-26)
if (s.guards.exists(_.chasing)) {
val alertText = "WARNING! ANIMAL GUARDS ARE CHASING YOU - RUN TO YOUR BASE!"
g.setColor(new Color(95,12,20,220)); g.fillRoundRect(w/2-260,56,520,38,12,12)
g.setFont(new Font(fontName,Font.BOLD,15)); g.setColor(new Color(255,220,215))
g.drawString(alertText,w/2-g.getFontMetrics.stringWidth(alertText)/2,81)
}
if (s.announcement.nonEmpty) {
g.setColor(new Color(0,0,0,165)); g.fillRoundRect(w/2-235,14,470,34,12,12)
g.setFont(new Font(fontName,Font.BOLD,13)); g.setColor(new Color(255,236,153))
val msg=s.announcement.take(65)
g.drawString(msg,w/2-g.getFontMetrics.stringWidth(msg)/2,36)
}
if (s.boostUntil > System.currentTimeMillis()) {
g.setFont(new Font(fontName,Font.BOLD,14)); g.setColor(new Color(255,220,88))
g.drawString("GLOBAL "+s.boostMultiplier+"x BOOST",w-205,80)
}
}
override def paintComponent(graphics: Graphics): Unit = {
super.paintComponent(graphics)
val g = graphics.create().asInstanceOf[Graphics2D]
try {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON)
val w=getWidth; val h=getHeight
g.setPaint(new GradientPaint(0,0,new Color(8,15,34),0,h/2,new Color(80,117,144)))
g.fillRect(0,0,w,h)
val horizon=(h*0.43).toInt
g.setPaint(new GradientPaint(0,horizon,new Color(87,130,145),0,h,new Color(9,25,30)))
g.fillRect(0,horizon,w,h-horizon)
val drawList=makeDrawList(w,h)
drawList.foreach { p =>
g.setColor(p.color)
g.fillPolygon(p.xs,p.ys,p.xs.length)
if (p.xs.length==3) {
g.setColor(new Color(255,255,255,22))
g.drawPolygon(p.xs,p.ys,3)
}
}
drawHud(g,w,h)
} finally g.dispose()
}
override def addNotify(): Unit = { super.addNotify(); requestFocusInWindow() }
override def removeNotify(): Unit = { timer.stop(); super.removeNotify() }
}
private def showGame(): Unit = {
profileStatsLabel = null
val root = new JPanel(new BorderLayout(8, 8)); root.setBackground(bg)
val top = panel(new BorderLayout(8, 4))
val title = label("STEAL AN EGG ? YADNESH EDITION", 22, gold)
title.setBorder(BorderFactory.createEmptyBorder(8, 12, 2, 8))
statusLabel = label("Connecting to world...", 13, cyan)
statusLabel.setBorder(BorderFactory.createEmptyBorder(0, 13, 8, 8))
val north = panel(new GridLayout(2, 1)); north.add(title); north.add(statusLabel)
top.add(north, BorderLayout.CENTER)
root.add(top, BorderLayout.NORTH)
gamePanel = new WorldPanel()
gamePanel.setPreferredSize(new Dimension(1000, 620))
root.add(gamePanel, BorderLayout.CENTER)
val bar = panel(new FlowLayout(FlowLayout.CENTER, 7, 7))
val train = button("? TREADMILL [T]")
val bag = button("? EGG BAG [I]")
val shop = button("? SHOP")
val biomesButton = button("? BIOMES [M]")
val admin = button("? ADMIN PANEL")
val profile = button("PROFILE")
val logout = button("LOG OUT")
train.addActionListener(_ => connection.send("TRAIN"))
bag.addActionListener(_ => showInventory())
shop.addActionListener(_ => showShop())
biomesButton.addActionListener(_ => showBiomes())
admin.addActionListener(_ => showAdminPanel())
profile.addActionListener(_ => showProfileHome())
logout.addActionListener(_ => {
if (connection != null) connection.close()
connection = null; gamePanel = null; profileStatsLabel = null; currentUsername = ""; showLogin()
})
bar.add(train); bar.add(bag); bar.add(shop); bar.add(biomesButton); bar.add(profile)
if (currentSnapshot.role == "OWNER" || currentSnapshot.role == "ADMIN") bar.add(admin)
bar.add(logout)
root.add(bar, BorderLayout.SOUTH)
screen(root, 1200, 820)
gamePanel.requestFocusInWindow()
if (connection != null) onSnapshot(connection.latest)
}
private def showInventory(): Unit = {
if (connection == null) return
val s = connection.latest
val dialog = new JDialog(frame, "EGG BAG & PET COLLECTION", true)
dialog.getContentPane.setBackground(panelBg)
val root = new JPanel(new BorderLayout(8, 8)); root.setBackground(panelBg)
val text = new JTextArea()
text.setEditable(false); text.setBackground(new Color(16, 23, 41)); text.setForeground(white)
text.setFont(new Font(fontName, Font.PLAIN, 14))
val now = System.currentTimeMillis()
val eggLines = if (s.eggs.isEmpty) "No eggs yet. Explore a biome and press E near an egg."
else s.eggs.map(e => e.name + " ? " + e.rarity + " ? " +
(if (now >= e.hatchAt) "READY TO HATCH" else "Hatches in " + ((e.hatchAt - now + 999) / 1000) + "s") +
" ? ID " + e.id).mkString("\n")
val petLines = if (s.pets.isEmpty) "No pets hatched yet."
else s.pets.map(p => p.name + " ? " + p.rarity + " ? +" + p.income + " coins/sec").mkString("\n")
text.setText("YOUR EGGS\n===========\n" + eggLines + "\n\nYOUR PETS\n=========\n" + petLines +
"\n\nPets automatically earn coins while you are connected.")
root.add(new JScrollPane(text), BorderLayout.CENTER)
val buttons = panel(new FlowLayout(FlowLayout.CENTER))
s.eggs.filter(_.hatchAt <= now).foreach { egg =>
val b = button("HATCH " + egg.name)
b.addActionListener(_ => { connection.send("HATCH", egg.id); dialog.dispose() })
buttons.add(b)
}
val close = button("CLOSE"); close.addActionListener(_ => dialog.dispose()); buttons.add(close)
root.add(buttons, BorderLayout.SOUTH)
dialog.setContentPane(root); dialog.setSize(620, 450); dialog.setLocationRelativeTo(frame); dialog.setVisible(true)
}
private def showShop(): Unit = {
if (connection == null) return
val s = connection.latest
val message = "Coins: " + fmt(s.coins) + "\nSpeed: " + s.speed + "\nBase level: " + s.baseLevel +
"\n\nTreadmill Speed Upgrade: 100 + 8 � current Speed coins (+20 Speed)." +
"\nBase Upgrade: 250 � current Base Level coins." +
"\n2x Server Boost: 500 coins for 60 seconds (shared server boost)."
val choice = JOptionPane.showOptionDialog(frame, message, "EGG SHOP",
JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null,
Array[Object]("UPGRADE SPEED", "UPGRADE BASE", "BUY 2x BOOST", "CANCEL"), "UPGRADE SPEED")
choice match {
case 0 => connection.send("UPGRADE_SPEED")
case 1 => connection.send("UPGRADE_BASE")
case 2 => connection.send("BUY_BOOST")
case _ => ()
}
}
private def showBiomes(): Unit = {
val s = if (connection == null) currentSnapshot else connection.latest
val body = biomes.map(b => {
val unlocked = s.speed >= b.gate
(if (unlocked) "? " else "? ") + b.name + " ? Required Speed: " + b.gate
}).mkString("\n")
showMessage("BIOME DISCOVERY\n\n" + body + "\n\nMove east to explore the next unlocked biome.", "BIOMES")
}
private def showAdminPanel(): Unit = {
if (connection == null || (connection.latest.role != "OWNER" && connection.latest.role != "ADMIN")) {
showError("You do not have admin permission."); return
}
val owner = connection.latest.role == "OWNER"
val root = new JPanel(new GridLayout(0, 2, 8, 8)); root.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12))
root.setBackground(panelBg)
val announce = new JTextField()
val eventTitle = new JTextField("Double Coins Weekend")
val eventText = new JTextField("Limited-time event is live!")
val spawnBiome = new JComboBox[String](biomes.map(_.name).toArray)
val spawnRarity = new JComboBox[String](Array("Common", "Uncommon", "Rare", "Epic", "Legendary", "Mythic", "Secret", "Celestial"))
val grantUser = new JTextField(); val grantAmount = new JTextField("1000")
val promoteUser = new JTextField(); val promoteRole = new JComboBox[String](Array("ADMIN", "PLAYER"))
val boostMult = new JTextField("2"); val boostSeconds = new JTextField("120")
val updateVersion = new JTextField("0.2.0"); val updateText = new JTextField("New biome and egg drop!")
def addRow(name: String, component: Component): Unit = { root.add(label(name, 12, white)); root.add(component) }
addRow("GLOBAL ANNOUNCEMENT", announce)
addRow("EVENT TITLE", eventTitle); addRow("EVENT MESSAGE", eventText)
addRow("SPAWN EGG BIOME", spawnBiome); addRow("SPAWN RARITY", spawnRarity)
addRow("BOOST MULTIPLIER", boostMult); addRow("BOOST SECONDS", boostSeconds)
addRow("UPDATE VERSION", updateVersion); addRow("UPDATE NOTES", updateText)
if (owner) {
addRow("PLAYER TO GRANT COINS", grantUser); addRow("COINS AMOUNT", grantAmount)
addRow("PLAYER TO PROMOTE", promoteUser); addRow("ROLE", promoteRole)
}
val buttons = panel(new FlowLayout(FlowLayout.CENTER))
val broadcast = button("SEND ANNOUNCEMENT")
val event = button("START LIVE EVENT")
val spawn = button("SPAWN EGG")
val boost = button("ACTIVATE BOOST")
val update = button("PUBLISH UPDATE NOTE")
val close = button("CLOSE")
broadcast.addActionListener(_ => connection.send("ANNOUNCE", announce.getText.trim))
event.addActionListener(_ => connection.send("EVENT", eventTitle.getText.trim, eventText.getText.trim))
spawn.addActionListener(_ => connection.send("SPAWN_EGG", spawnBiome.getSelectedItem.toString, spawnRarity.getSelectedItem.toString))
boost.addActionListener(_ => connection.send("BOOST", boostMult.getText.trim, boostSeconds.getText.trim))
update.addActionListener(_ => connection.send("UPDATE_NOTE", updateVersion.getText.trim, updateText.getText.trim))
close.addActionListener(_ => SwingUtilities.getWindowAncestor(root).dispose())
buttons.add(broadcast); buttons.add(event); buttons.add(spawn); buttons.add(boost); buttons.add(update)
if (owner) {
val grant = button("GRANT COINS")
grant.addActionListener(_ => connection.send("GRANT_COINS", grantUser.getText.trim, grantAmount.getText.trim))
val promote = button("CHANGE PLAYER ROLE")
promote.addActionListener(_ => connection.send("PROMOTE", promoteUser.getText.trim, promoteRole.getSelectedItem.toString))
buttons.add(grant); buttons.add(promote)
}
buttons.add(close)
val wrapper = new JPanel(new BorderLayout(8, 8)); wrapper.setBackground(panelBg)
wrapper.add(new JScrollPane(root), BorderLayout.CENTER); wrapper.add(buttons, BorderLayout.SOUTH)
val dialog = new JDialog(frame, "YADNESH OWNER / ADMIN CONTROL PANEL", false)
dialog.setContentPane(wrapper); dialog.setSize(760, 650); dialog.setLocationRelativeTo(frame); dialog.setVisible(true)
}
def main(args: Array[String]): Unit = SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName)
frame = new JFrame("STEAL AN EGG - YADNESH EDITION")
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
showLogin()
}
})
}
// Kojo script entry point. Start the local server from the login screen.
StealAnEggClient.main(Array.empty[String])