Code Sketch
Bixuu spring season event 5
Category: Programming
import java.awt.{BasicStroke, BorderLayout, CardLayout, Color, Dimension, Font, GradientPaint, Graphics, Graphics2D, GridLayout, Polygon, RenderingHints}
import java.awt.event.{ActionEvent, ActionListener, KeyEvent, MouseAdapter, MouseEvent}
import java.awt.geom.{Ellipse2D, RoundRectangle2D}
import java.io.{BufferedReader, InputStreamReader, PrintWriter}
import java.net.{InetAddress, InetSocketAddress, ServerSocket, Socket}
import java.util.prefs.Preferences
import javax.swing.{AbstractAction, BorderFactory, Box, BoxLayout, JButton, JComboBox, JComponent, JFrame, JLabel, JOptionPane, JPanel, JScrollPane, JTextField, KeyStroke, SwingConstants, SwingUtilities, Timer, WindowConstants}
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
/**
* SPRING SEASON EVENT 5 - NATUREVERSE: THE FUTURE BLOOM
* Standalone Java Swing game for Kojo / Scala desktop environments.
* No third-party libraries are required.
*
* IMPORTANT:
* - The login is a local player profile, not an online account.
* - Wi-Fi multiplayer is LAN-only: both computers must use the same local Wi-Fi.
* - Multiplayer shares player positions and names. Each computer runs its own
* level simulation and local progress; this is a lightweight LAN co-op demo.
*
* Controls: click ground to move, WASD / arrows to move, SPACE to attack,
* Q to use equipped power, P to pause, R to restart, ENTER for next level
* after completing a level, and B to return to the level selection screen.
*/
object BixuuSpringSeasonEvent5 {
private val AppTitle = "SPRING SEASON EVENT 5 - NATUREVERSE: THE FUTURE BLOOM"
private val prefs = Preferences.userRoot().node("BixuuSpringSeasonEvent5")
private case class PowerDef(name: String, unlockAt: Int, cost: Int, description: String, effect: String)
private case class GearDef(name: String, cost: Int, reduction: Double, kind: String)
private class Enemy(var x: Double, var z: Double, var hp: Int, val maxHp: Int,
val damage: Int, val boss: Boolean) {
var attackCooldown = 0.0
}
private class BloomOrb(val x: Double, val z: Double) { var collected = false }
private case class ScreenPoint(x: Int, y: Int, scale: Double, depth: Double)
private case class RemoteSnapshot(name: String, level: Int, x: Double, z: Double,
health: Int, avatar: Int)
private val powers = Array(
PowerDef("Nature Pulse", 1, 0, "A focused nature burst that damages a nearby guardian.", "pulse"),
PowerDef("Thorn Shield", 2, 25, "Reduce incoming damage for a short time.", "shield"),
PowerDef("Bloom Dash", 4, 40, "Move faster for a few seconds.", "dash"),
PowerDef("Healing Bloom", 6, 55, "Restore a large amount of health.", "heal"),
PowerDef("Storm Vines", 8, 70, "Damage every nearby guardian with nature vines.", "storm"),
PowerDef("Sun Burst", 10, 90, "A strong burst against the nearest guardian.", "sun"),
PowerDef("Crystal Guard", 12, 110, "A longer, stronger defensive shield.", "crystal"),
PowerDef("Starfall Bloom", 14, 130, "A powerful area attack around your hero.", "starfall"),
PowerDef("Phoenix Bloom", 16, 150, "Restore health and gain a short shield.", "phoenix"),
PowerDef("Nature Nova", 18, 180, "A huge nature pulse that hits nearby enemies.", "nova"),
PowerDef("World Tree", 20, 220, "The ultimate power: heal and strike all guardians.", "worldtree")
)
private val armors = Array(
GearDef("No Armor", 0, 0.0, "armor"),
GearDef("Leaf Armor", 25, 0.10, "armor"),
GearDef("Crystal Armor", 60, 0.20, "armor"),
GearDef("Solar Armor", 110, 0.32, "armor")
)
private val shields = Array(
GearDef("No Shield", 0, 0.0, "shield"),
GearDef("Petal Shield", 20, 0.08, "shield"),
GearDef("Moon Shield", 55, 0.16, "shield"),
GearDef("World Shield", 100, 0.25, "shield")
)
private val avatars = Array("Forest Scout", "Bloom Mage", "Crystal Knight", "Sun Guardian")
private var playerName = prefs.get("playerName", "BloomHero")
private var coins = math.max(0, prefs.getInt("coins", 0))
private var highestUnlocked = math.max(1, math.min(20, prefs.getInt("highestUnlocked", 1)))
private var avatarIndex = math.max(0, math.min(avatars.length - 1, prefs.getInt("avatarIndex", 0)))
private val ownedPowers = mutable.Set[String]()
private val ownedArmors = mutable.Set[String]()
private val ownedShields = mutable.Set[String]()
private var equippedPower = prefs.get("equippedPower", "Nature Pulse")
private var equippedArmor = prefs.get("equippedArmor", "No Armor")
private var equippedShield = prefs.get("equippedShield", "No Shield")
private var frame: JFrame = null
private var root: JPanel = null
private var cards: CardLayout = null
private var nameField: JTextField = null
private var hostPortField: JTextField = null
private var joinIpField: JTextField = null
private var joinPortField: JTextField = null
private var levelsCard: JPanel = null
private var shopCard: JPanel = null
private var avatarCard: JPanel = null
private var gamePanel: GamePanel = null
private var nextButton: JButton = null
private var statusLabel: JLabel = null
private var gameTimer: Timer = null
private var currentLevel = 1
private var playerX = 0.0
private var playerZ = 0.0
private var targetX = 0.0
private var targetZ = 0.0
private var hasMoveTarget = false
private var health = 100
private var attackCooldown = 0.0
private var powerCooldown = 0.0
private var hurtCooldown = 0.0
private var shieldTime = 0.0
private var dashTime = 0.0
private var attackFlash = 0.0
private var paused = false
private var levelComplete = false
private var gameOver = false
private var rewardGiven = false
private var kills = 0
private var collectedOrbs = 0
private var message = "Welcome to Natureverse!"
private var messageTime = 4.0
private var elapsed = 0.0
private val pressedKeys = mutable.Set[String]()
private val enemies = ArrayBuffer[Enemy]()
private val orbs = ArrayBuffer[BloomOrb]()
private var hostServer: HostServer = null
private var lanClient: LanLink = null
private var multiplayerMode = false
private var networkLabel = "Single Player"
private val skyDark = new Color(8, 24, 52)
private val skyLight = new Color(94, 186, 174)
private val mint = new Color(93, 245, 202)
private val gold = new Color(255, 218, 104)
private val deepGreen = new Color(10, 62, 49)
private def saveProgress(): Unit = {
try {
prefs.put("playerName", playerName)
prefs.putInt("coins", coins)
prefs.putInt("highestUnlocked", highestUnlocked)
prefs.putInt("avatarIndex", avatarIndex)
prefs.put("equippedPower", equippedPower)
prefs.put("equippedArmor", equippedArmor)
prefs.put("equippedShield", equippedShield)
prefs.put("ownedPowers", ownedPowers.mkString(","))
prefs.put("ownedArmors", ownedArmors.mkString(","))
prefs.put("ownedShields", ownedShields.mkString(","))
prefs.flush()
} catch { case _: Throwable => () }
}
private def loadInventory(): Unit = {
ownedPowers.clear()
ownedArmors.clear()
ownedShields.clear()
ownedPowers += "Nature Pulse"
ownedArmors += "No Armor"
ownedShields += "No Shield"
prefs.get("ownedPowers", "").split(",").filter(_.trim.nonEmpty).foreach(ownedPowers += _)
prefs.get("ownedArmors", "").split(",").filter(_.trim.nonEmpty).foreach(ownedArmors += _)
prefs.get("ownedShields", "").split(",").filter(_.trim.nonEmpty).foreach(ownedShields += _)
if (!ownedPowers.contains(equippedPower)) equippedPower = "Nature Pulse"
if (!ownedArmors.contains(equippedArmor)) equippedArmor = "No Armor"
if (!ownedShields.contains(equippedShield)) equippedShield = "No Shield"
}
private def clamp(v: Double, low: Double, high: Double): Double = math.max(low, math.min(high, v))
private def distance(ax: Double, az: Double, bx: Double, bz: Double): Double = {
val dx = ax - bx
val dz = az - bz
math.sqrt(dx * dx + dz * dz)
}
private def showMessage(text: String, seconds: Double): Unit = { message = text; messageTime = seconds }
private def screen(name: String): Unit = if (cards != null && root != null) {
cards.show(root, name)
root.revalidate()
root.repaint()
}
private def makeButton(text: String, color: Color = new Color(22, 100, 82))(action: => Unit): JButton = {
val b = new JButton(text)
b.setFont(new Font("SansSerif", Font.BOLD, 15))
b.setForeground(Color.WHITE)
b.setBackground(color)
b.setFocusPainted(false)
b.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(150, 255, 224, 150), 1),
BorderFactory.createEmptyBorder(9, 16, 9, 16)))
b.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = action })
b
}
private def heading(text: String): JLabel = {
val l = new JLabel(text, SwingConstants.CENTER)
l.setFont(new Font("SansSerif", Font.BOLD, 29))
l.setForeground(new Color(183, 255, 224))
l
}
private def subheading(text: String): JLabel = {
val l = new JLabel(text, SwingConstants.CENTER)
l.setFont(new Font("SansSerif", Font.PLAIN, 14))
l.setForeground(new Color(223, 244, 238))
l
}
private def baseScreen(): JPanel = {
val p = new JPanel()
p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS))
p.setBackground(new Color(7, 27, 37))
p.setBorder(BorderFactory.createEmptyBorder(24, 28, 24, 28))
p
}
private def addCentered(p: JPanel, c: JComponent): Unit = {
c.setAlignmentX(java.awt.Component.CENTER_ALIGNMENT)
p.add(c)
}
private def buildLoginCard(): JPanel = {
val p = baseScreen()
p.add(Box.createVerticalGlue())
addCentered(p, heading("SPRING SEASON EVENT 5"))
addCentered(p, heading("NATUREVERSE: THE FUTURE BLOOM"))
p.add(Box.createVerticalStrut(10))
addCentered(p, subheading("A 20-level 3D-style adventure ? powers ? gear ? LAN co-op"))
p.add(Box.createVerticalStrut(28))
addCentered(p, new JLabel("ENTER YOUR PLAYER NAME"))
nameField = new JTextField(playerName, 22)
nameField.setMaximumSize(new Dimension(330, 38))
nameField.setFont(new Font("SansSerif", Font.PLAIN, 18))
addCentered(p, nameField)
p.add(Box.createVerticalStrut(12))
addCentered(p, subheading("Local profile only. Do not enter a real password."))
p.add(Box.createVerticalStrut(15))
addCentered(p, makeButton("LOGIN / START", new Color(24, 142, 102)) {
val candidate = nameField.getText.trim.replace("|", "").replace("\n", "")
if (candidate.isEmpty) JOptionPane.showMessageDialog(frame, "Please enter a player name.", "Login", JOptionPane.INFORMATION_MESSAGE)
else {
playerName = candidate.take(18)
saveProgress()
screen("menu")
}
})
p.add(Box.createVerticalGlue())
p
}
private def buildMenuCard(): JPanel = {
val p = baseScreen()
p.add(Box.createVerticalGlue())
addCentered(p, heading("WELCOME, " + playerName.toUpperCase))
addCentered(p, subheading("Choose your adventure mode"))
p.add(Box.createVerticalStrut(24))
addCentered(p, makeButton("SINGLE PLAYER ADVENTURE") {
stopNetwork()
multiplayerMode = false
networkLabel = "Single Player"
refreshLevelsCard()
screen("levels")
})
p.add(Box.createVerticalStrut(10))
addCentered(p, makeButton("WI-FI MULTIPLAYER ? HOST / JOIN", new Color(34, 114, 145)) { screen("lan") })
p.add(Box.createVerticalStrut(10))
addCentered(p, makeButton("AVATAR ? ARMOR ? SHIELD ? POWERS", new Color(91, 91, 151)) {
refreshAvatarCard(); screen("avatar")
})
p.add(Box.createVerticalStrut(10))
addCentered(p, makeButton("POWER & GEAR SHOP", new Color(132, 91, 37)) { refreshShopCard(); screen("shop") })
p.add(Box.createVerticalStrut(10))
addCentered(p, makeButton("HOW TO PLAY", new Color(69, 91, 77)) { showHelp() })
p.add(Box.createVerticalStrut(10))
addCentered(p, makeButton("LOG OUT", new Color(91, 57, 69)) { screen("login") })
p.add(Box.createVerticalGlue())
p
}
private def buildLanCard(): JPanel = {
val p = baseScreen()
p.add(Box.createVerticalGlue())
addCentered(p, heading("WI-FI MULTIPLAYER"))
addCentered(p, subheading("Both players must be connected to the same Wi-Fi network."))
p.add(Box.createVerticalStrut(25))
addCentered(p, subheading("HOST A GAME ? SHARE YOUR LOCAL IP AND PORT"))
hostPortField = new JTextField("5055", 10)
hostPortField.setMaximumSize(new Dimension(180, 36))
addCentered(p, hostPortField)
addCentered(p, makeButton("START HOST SERVER", new Color(21, 126, 91)) { startHosting() })
p.add(Box.createVerticalStrut(24))
addCentered(p, subheading("JOIN A FRIEND ? ENTER THE HOST'S LOCAL IP"))
joinIpField = new JTextField("192.168.1.10", 18)
joinIpField.setMaximumSize(new Dimension(280, 36))
addCentered(p, joinIpField)
joinPortField = new JTextField("5055", 10)
joinPortField.setMaximumSize(new Dimension(180, 36))
addCentered(p, joinPortField)
addCentered(p, makeButton("JOIN WI-FI GAME", new Color(31, 101, 154)) { joinGame() })
p.add(Box.createVerticalStrut(18))
addCentered(p, subheading("LAN demo: player positions are shared; each PC saves its own progress."))
addCentered(p, makeButton("BACK TO MAIN MENU", new Color(73, 78, 86)) { screen("menu") })
p.add(Box.createVerticalGlue())
p
}
private def buildLevelsCard(): JPanel = {
levelsCard = new JPanel(new BorderLayout(12, 12))
levelsCard.setBackground(new Color(7, 27, 37))
levelsCard.setBorder(BorderFactory.createEmptyBorder(20, 24, 20, 24))
return levelsCard
}
private def refreshLevelsCard(): Unit = {
if (levelsCard == null) return
levelsCard.removeAll()
val top = new JPanel(new BorderLayout())
top.setOpaque(false)
val title = heading("CHOOSE YOUR LEVEL")
top.add(title, BorderLayout.CENTER)
val info = new JLabel("Player: " + playerName + " Coins: " + coins + " Mode: " + networkLabel, SwingConstants.CENTER)
info.setForeground(gold)
info.setFont(new Font("SansSerif", Font.BOLD, 14))
top.add(info, BorderLayout.SOUTH)
levelsCard.add(top, BorderLayout.NORTH)
val grid = new JPanel(new GridLayout(4, 5, 12, 12))
grid.setOpaque(false)
var level = 1
while (level <= 20) {
val n = level
val unlocked = n <= highestUnlocked
val suffix = if (n == 20) " ? FINAL BOSS" else if (n % 5 == 0) " ? BOSS" else ""
val b = makeButton((if (unlocked) "LEVEL " else "LOCKED ") + n + suffix,
if (!unlocked) new Color(55, 60, 68) else if (n == 20) new Color(139, 66, 90) else new Color(24 + n * 2, 105, 78)) {
if (unlocked) { startLevel(n); screen("game") }
}
b.setEnabled(unlocked)
grid.add(b)
level += 1
}
levelsCard.add(grid, BorderLayout.CENTER)
val bottom = new JPanel()
bottom.setOpaque(false)
bottom.add(makeButton("AVATAR / GEAR", new Color(84, 81, 144)) { refreshAvatarCard(); screen("avatar") })
bottom.add(makeButton("POWER SHOP", new Color(130, 92, 38)) { refreshShopCard(); screen("shop") })
bottom.add(makeButton("MAIN MENU", new Color(69, 77, 84)) { screen("menu") })
levelsCard.add(bottom, BorderLayout.SOUTH)
levelsCard.revalidate()
levelsCard.repaint()
}
private def buildAvatarCard(): JPanel = {
avatarCard = new JPanel(new BorderLayout(12, 12))
avatarCard.setBackground(new Color(7, 27, 37))
avatarCard.setBorder(BorderFactory.createEmptyBorder(22, 30, 22, 30))
return avatarCard
}
private def refreshAvatarCard(): Unit = {
if (avatarCard == null) return
avatarCard.removeAll()
avatarCard.add(heading("SELECT YOUR HERO"), BorderLayout.NORTH)
val center = new JPanel()
center.setOpaque(false)
center.setLayout(new BoxLayout(center, BoxLayout.Y_AXIS))
val avatarBox = new JComboBox[String](avatars)
avatarBox.setSelectedIndex(avatarIndex)
avatarBox.setMaximumSize(new Dimension(300, 36))
avatarBox.setFont(new Font("SansSerif", Font.BOLD, 16))
avatarBox.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = {
avatarIndex = avatarBox.getSelectedIndex; saveProgress()
}})
addCentered(center, subheading("AVATAR")); addCentered(center, avatarBox)
center.add(Box.createVerticalStrut(18))
addCentered(center, subheading("EQUIPPED ARMOR: " + equippedArmor))
addCentered(center, makeButton("CHOOSE OWNED ARMOR", new Color(67, 94, 131)) {
val choices: Array[Object] = ownedArmors.toArray.sorted.map(x => x.asInstanceOf[Object])
val selected = JOptionPane.showInputDialog(frame, "Choose armor:", "Armor", JOptionPane.PLAIN_MESSAGE, null, choices, equippedArmor)
if (selected != null) { equippedArmor = selected.toString; saveProgress(); refreshAvatarCard() }
})
center.add(Box.createVerticalStrut(12))
addCentered(center, subheading("EQUIPPED SHIELD: " + equippedShield))
addCentered(center, makeButton("CHOOSE OWNED SHIELD", new Color(67, 94, 131)) {
val choices: Array[Object] = ownedShields.toArray.sorted.map(x => x.asInstanceOf[Object])
val selected = JOptionPane.showInputDialog(frame, "Choose shield:", "Shield", JOptionPane.PLAIN_MESSAGE, null, choices, equippedShield)
if (selected != null) { equippedShield = selected.toString; saveProgress(); refreshAvatarCard() }
})
center.add(Box.createVerticalStrut(12))
addCentered(center, subheading("EQUIPPED POWER: " + equippedPower))
addCentered(center, makeButton("SELECT YOUR POWER", new Color(105, 75, 145)) {
val choices: Array[Object] = ownedPowers.toArray.sorted.map(x => x.asInstanceOf[Object])
val selected = JOptionPane.showInputDialog(frame, "Choose your active power:", "Select Your Power", JOptionPane.PLAIN_MESSAGE, null, choices, equippedPower)
if (selected != null) { equippedPower = selected.toString; saveProgress(); refreshAvatarCard() }
})
center.add(Box.createVerticalStrut(15))
addCentered(center, subheading("COINS: " + coins + " ? Highest unlocked level: " + highestUnlocked))
avatarCard.add(center, BorderLayout.CENTER)
val bottom = new JPanel(); bottom.setOpaque(false)
bottom.add(makeButton("POWER & GEAR SHOP", new Color(130, 92, 38)) { refreshShopCard(); screen("shop") })
bottom.add(makeButton("BACK TO LEVELS", new Color(24, 110, 80)) { refreshLevelsCard(); screen("levels") })
avatarCard.add(bottom, BorderLayout.SOUTH)
avatarCard.revalidate(); avatarCard.repaint()
}
private def buildShopCard(): JPanel = {
shopCard = new JPanel(new BorderLayout(12, 12))
shopCard.setBackground(new Color(7, 27, 37))
shopCard.setBorder(BorderFactory.createEmptyBorder(18, 24, 18, 24))
return shopCard
}
private def refreshShopCard(): Unit = {
if (shopCard == null) return
shopCard.removeAll()
val top = new JPanel(new BorderLayout())
top.setOpaque(false)
top.add(heading("POWER & GEAR SHOP"), BorderLayout.CENTER)
val coinLabel = new JLabel("YOUR COINS: " + coins + " ? Powers unlock every two levels", SwingConstants.CENTER)
coinLabel.setForeground(gold); coinLabel.setFont(new Font("SansSerif", Font.BOLD, 15))
top.add(coinLabel, BorderLayout.SOUTH)
shopCard.add(top, BorderLayout.NORTH)
val list = new JPanel()
list.setOpaque(false)
list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS))
val powerTitle = new JLabel("POWERS ? Q TO USE IN GAME")
powerTitle.setForeground(mint); powerTitle.setFont(new Font("SansSerif", Font.BOLD, 18))
list.add(powerTitle)
powers.foreach { power =>
val row = new JPanel(new BorderLayout(10, 4)); row.setOpaque(false)
val owned = ownedPowers.contains(power.name)
val available = highestUnlocked >= power.unlockAt
val text = new JLabel(power.name + " ? " + power.description + " [Level " + power.unlockAt + "]")
text.setForeground(Color.WHITE); text.setFont(new Font("SansSerif", Font.PLAIN, 13))
row.add(text, BorderLayout.CENTER)
val label = if (owned) { if (equippedPower == power.name) "EQUIPPED" else "EQUIP" }
else if (!available) "LOCKED"
else "BUY " + power.cost + " COINS"
val actionButton = makeButton(label, if (owned) new Color(45, 117, 88) else new Color(104, 78, 133)) {
if (!available && !owned) JOptionPane.showMessageDialog(frame, "Unlock this power by reaching level " + power.unlockAt + ".", "Power locked", JOptionPane.INFORMATION_MESSAGE)
else if (owned) { equippedPower = power.name; saveProgress(); refreshShopCard() }
else if (coins >= power.cost) {
coins -= power.cost; ownedPowers += power.name; equippedPower = power.name; saveProgress(); refreshShopCard()
} else JOptionPane.showMessageDialog(frame, "You need " + (power.cost - coins) + " more coins.", "Not enough coins", JOptionPane.INFORMATION_MESSAGE)
}
row.add(actionButton, BorderLayout.EAST)
list.add(row); list.add(Box.createVerticalStrut(5))
}
val gearTitle = new JLabel("ARMOR & SHIELDS")
gearTitle.setForeground(gold); gearTitle.setFont(new Font("SansSerif", Font.BOLD, 18))
list.add(Box.createVerticalStrut(8)); list.add(gearTitle)
(armors ++ shields).filter(_.cost > 0).foreach { gear =>
val owned = if (gear.kind == "armor") ownedArmors.contains(gear.name) else ownedShields.contains(gear.name)
val equipped = if (gear.kind == "armor") equippedArmor == gear.name else equippedShield == gear.name
val row = new JPanel(new BorderLayout(10, 4)); row.setOpaque(false)
val item = new JLabel(gear.name + " ? damage reduction " + (gear.reduction * 100).toInt + "%")
item.setForeground(Color.WHITE); item.setFont(new Font("SansSerif", Font.PLAIN, 13))
row.add(item, BorderLayout.CENTER)
val b = makeButton(if (equipped) "EQUIPPED" else if (owned) "EQUIP" else "BUY " + gear.cost,
new Color(91, 101, 125)) {
val inventory = if (gear.kind == "armor") ownedArmors else ownedShields
if (inventory.contains(gear.name)) {
if (gear.kind == "armor") equippedArmor = gear.name else equippedShield = gear.name
saveProgress(); refreshShopCard()
} else if (coins >= gear.cost) {
coins -= gear.cost; inventory += gear.name
if (gear.kind == "armor") equippedArmor = gear.name else equippedShield = gear.name
saveProgress(); refreshShopCard()
} else JOptionPane.showMessageDialog(frame, "Not enough coins for " + gear.name + ".", "Shop", JOptionPane.INFORMATION_MESSAGE)
}
row.add(b, BorderLayout.EAST); list.add(row); list.add(Box.createVerticalStrut(5))
}
shopCard.add(new JScrollPane(list), BorderLayout.CENTER)
val bottom = new JPanel(); bottom.setOpaque(false)
bottom.add(makeButton("AVATAR & EQUIPMENT", new Color(84, 81, 144)) { refreshAvatarCard(); screen("avatar") })
bottom.add(makeButton("BACK TO LEVELS", new Color(24, 110, 80)) { refreshLevelsCard(); screen("levels") })
shopCard.add(bottom, BorderLayout.SOUTH)
shopCard.revalidate(); shopCard.repaint()
}
private def buildGameCard(): JPanel = {
val p = new JPanel(new BorderLayout())
p.setBackground(Color.BLACK)
gamePanel = new GamePanel()
p.add(gamePanel, BorderLayout.CENTER)
val controls = new JPanel()
controls.setBackground(new Color(6, 24, 32))
controls.add(makeButton("PAUSE", new Color(77, 89, 103)) { paused = !paused; showMessage(if (paused) "Paused" else "Back to adventure", 2.0) })
controls.add(makeButton("USE POWER [Q]", new Color(100, 72, 143)) { activatePower() })
controls.add(makeButton("POWER SHOP", new Color(132, 91, 37)) { refreshShopCard(); screen("shop") })
controls.add(makeButton("LEVELS [B]", new Color(28, 112, 81)) { returnToLevels() })
nextButton = makeButton("NEXT LEVEL [ENTER]", new Color(34, 145, 96)) { nextLevel() }
nextButton.setEnabled(false)
controls.add(nextButton)
p.add(controls, BorderLayout.SOUTH)
gameTimer = new Timer(16, new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
val now = System.nanoTime()
val dt = math.min(0.05, (now - lastTick) / 1000000000.0)
lastTick = now
if (!paused && !levelComplete && !gameOver) updateGame(dt)
if (gamePanel != null) gamePanel.repaint()
}
})
p
}
private var lastTick = System.nanoTime()
private def buildCards(): Unit = {
cards = new CardLayout()
root = new JPanel(cards)
root.add(buildLoginCard(), "login")
root.add(buildMenuCard(), "menu")
root.add(buildLanCard(), "lan")
root.add(buildLevelsCard(), "levels")
root.add(buildAvatarCard(), "avatar")
root.add(buildShopCard(), "shop")
root.add(buildGameCard(), "game")
}
private def launch(): Unit = {
loadInventory()
frame = new JFrame(AppTitle)
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.setMinimumSize(new Dimension(940, 650))
buildCards()
frame.setContentPane(root)
frame.setSize(1280, 820)
frame.setLocationRelativeTo(null)
frame.setVisible(true)
if (playerName == null || playerName.trim.isEmpty) screen("login") else screen("login")
}
private def showHelp(): Unit = {
val help = "HOW TO PLAY\n\n" +
"LOGIN: Enter a local player display name. No password or online account is used.\n\n" +
"MODES: Single Player or Wi-Fi Multiplayer (Host / Join on the same Wi-Fi).\n\n" +
"MOVE: Click the ground, or use W A S D / Arrow Keys.\n" +
"ATTACK: SPACE. POWER: Q. PAUSE: P. RESTART LEVEL: R.\n" +
"NEXT LEVEL AFTER WIN: ENTER. BACK TO LEVEL SELECT: B.\n\n" +
"MISSION: Collect all Bloom Orbs, defeat every guardian, then reach the portal.\n" +
"Complete levels to earn coins. Powers become available every two levels.\n" +
"Use the Shop to buy powers, armor and shields, then equip them.\n\n" +
"The 3D look is a perspective-rendered Swing game, not a full 3D engine."
JOptionPane.showMessageDialog(frame, help, "Natureverse - Help", JOptionPane.INFORMATION_MESSAGE)
}
private def parsePort(text: String): Int = {
val p = text.trim.toInt
if (p < 1024 || p > 65535) throw new IllegalArgumentException("Port must be between 1024 and 65535.")
p
}
private def startHosting(): Unit = {
val port = try parsePort(hostPortField.getText) catch {
case e: Throwable => JOptionPane.showMessageDialog(frame, e.getMessage, "Invalid port", JOptionPane.ERROR_MESSAGE); return
}
val t = new Thread(new Runnable {
override def run(): Unit = {
try {
stopNetwork()
val server = new HostServer(port)
hostServer = server
multiplayerMode = true
networkLabel = "Wi-Fi HOST : " + port
val ip = try InetAddress.getLocalHost.getHostAddress catch { case _: Throwable => "check your Wi-Fi adapter" }
SwingUtilities.invokeLater(new Runnable { override def run(): Unit = {
JOptionPane.showMessageDialog(frame,
"HOST SERVER READY\n\nShare this local IP with the other player:\n" + ip +
"\nPort: " + port + "\n\nBoth players should select the same level.",
"Wi-Fi Multiplayer", JOptionPane.INFORMATION_MESSAGE)
refreshLevelsCard(); screen("levels")
}})
} catch {
case e: Throwable => SwingUtilities.invokeLater(new Runnable { override def run(): Unit =
JOptionPane.showMessageDialog(frame, "Could not start host server: " + e.toString, "LAN error", JOptionPane.ERROR_MESSAGE)
})
}
}
}, "Natureverse-Host-Start")
t.setDaemon(true); t.start()
}
private def joinGame(): Unit = {
val host = joinIpField.getText.trim
val port = try parsePort(joinPortField.getText) catch {
case e: Throwable => JOptionPane.showMessageDialog(frame, e.getMessage, "Invalid port", JOptionPane.ERROR_MESSAGE); return
}
if (host.isEmpty) { JOptionPane.showMessageDialog(frame, "Enter the host computer's local IP.", "Join game", JOptionPane.INFORMATION_MESSAGE); return }
val t = new Thread(new Runnable {
override def run(): Unit = {
try {
stopNetwork()
val socket = new Socket()
socket.connect(new InetSocketAddress(host, port), 5000)
lanClient = new LanLink(socket)
multiplayerMode = true
networkLabel = "Wi-Fi CLIENT"
SwingUtilities.invokeLater(new Runnable { override def run(): Unit = { refreshLevelsCard(); screen("levels") } })
} catch {
case e: Throwable => SwingUtilities.invokeLater(new Runnable { override def run(): Unit =
JOptionPane.showMessageDialog(frame,
"Could not join. Check that both PCs use the same Wi-Fi, the host is running, and the IP/port are correct.\n\n" + e.toString,
"LAN connection failed", JOptionPane.ERROR_MESSAGE)
})
}
}
}, "Natureverse-LAN-Join")
t.setDaemon(true); t.start()
}
private def stopNetwork(): Unit = {
try { if (hostServer != null) hostServer.close() } catch { case _: Throwable => () }
try { if (lanClient != null) lanClient.close() } catch { case _: Throwable => () }
hostServer = null; lanClient = null
}
private def networkSend(): Unit = {
val safeName = playerName.replace("|", " ").replace("\n", " ").take(18)
val line = safeName + "|" + currentLevel + "|" + playerX + "|" + playerZ + "|" + health + "|" + avatarIndex
try {
if (hostServer != null) hostServer.send(line)
else if (lanClient != null) lanClient.send(line)
} catch { case _: Throwable => () }
}
private def remotePlayer: Option[RemoteSnapshot] = {
if (hostServer != null) hostServer.latest
else if (lanClient != null) lanClient.latest
else None
}
private def startLevel(level: Int): Unit = {
currentLevel = math.max(1, math.min(20, level))
playerX = 0.0; playerZ = 0.0; targetX = 0.0; targetZ = 0.0; hasMoveTarget = false
health = 100; attackCooldown = 0.0; powerCooldown = 0.0; hurtCooldown = 0.0
shieldTime = 0.0; dashTime = 0.0; attackFlash = 0.0
paused = false; levelComplete = false; gameOver = false; rewardGiven = false
kills = 0; collectedOrbs = 0; elapsed = 0.0; pressedKeys.clear()
enemies.clear(); orbs.clear()
val enemyCount = 2 + (currentLevel - 1) / 2
val baseHp = 1 + (currentLevel - 1) / 5
val enemyDamage = 5 + currentLevel / 3
var i = 0
while (i < enemyCount) {
val x = (((i * 173 + currentLevel * 41) % 660) - 330).toDouble
val z = 230.0 + i * (650.0 / math.max(1, enemyCount - 1))
val boss = (currentLevel % 5 == 0 && i == enemyCount - 1) || (currentLevel == 20 && i == enemyCount - 1)
val hp = baseHp + (if (boss) 5 + currentLevel / 4 else 0)
enemies += new Enemy(x, z, hp, hp, enemyDamage + (if (boss) 4 else 0), boss)
i += 1
}
val orbCount = math.min(4, 1 + (currentLevel - 1) / 5)
var j = 0
while (j < orbCount) {
val x = (((j * 227 + currentLevel * 63) % 520) - 260).toDouble
val z = 180.0 + j * (710.0 / math.max(1, orbCount - 1))
orbs += new BloomOrb(x, z)
j += 1
}
showMessage("LEVEL " + currentLevel + " ? collect " + orbCount + " Bloom Orbs, defeat " + enemyCount + " guardians, reach the portal!", 5.0)
if (nextButton != null) nextButton.setEnabled(false)
if (gameTimer != null) { lastTick = System.nanoTime(); gameTimer.start() }
if (gamePanel != null) SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = gamePanel.requestFocusInWindow()
})
}
private def currentArmorReduction: Double = armors.find(_.name == equippedArmor).map(_.reduction).getOrElse(0.0)
private def currentShieldReduction: Double = shields.find(_.name == equippedShield).map(_.reduction).getOrElse(0.0)
private def requiredOrbs: Int = orbs.length
private def objectiveComplete: Boolean = collectedOrbs >= requiredOrbs && enemies.forall(_.hp <= 0)
private def updateGame(dt: Double): Unit = {
elapsed += dt
if (messageTime > 0.0) messageTime -= dt
if (attackCooldown > 0.0) attackCooldown -= dt
if (powerCooldown > 0.0) powerCooldown -= dt
if (hurtCooldown > 0.0) hurtCooldown -= dt
if (shieldTime > 0.0) shieldTime -= dt
if (dashTime > 0.0) dashTime -= dt
if (attackFlash > 0.0) attackFlash -= dt
var dx = 0.0; var dz = 0.0
if (pressedKeys.contains("W") || pressedKeys.contains("UP")) dz += 1.0
if (pressedKeys.contains("S") || pressedKeys.contains("DOWN")) dz -= 1.0
if (pressedKeys.contains("A") || pressedKeys.contains("LEFT")) dx -= 1.0
if (pressedKeys.contains("D") || pressedKeys.contains("RIGHT")) dx += 1.0
if (dx != 0.0 || dz != 0.0) {
hasMoveTarget = false
val len = math.sqrt(dx * dx + dz * dz); dx /= len; dz /= len
val speed = (205.0 + currentLevel * 3.0) * (if (dashTime > 0.0) 1.85 else 1.0)
playerX = clamp(playerX + dx * speed * dt, -470.0, 470.0)
playerZ = clamp(playerZ + dz * speed * dt, -60.0, 1040.0)
} else if (hasMoveTarget) {
val tx = targetX - playerX; val tz = targetZ - playerZ
val d = math.sqrt(tx * tx + tz * tz)
if (d < 8.0) hasMoveTarget = false
else {
val speed = (220.0 + currentLevel * 2.0) * (if (dashTime > 0.0) 1.8 else 1.0)
playerX = clamp(playerX + tx / d * speed * dt, -470.0, 470.0)
playerZ = clamp(playerZ + tz / d * speed * dt, -60.0, 1040.0)
}
}
orbs.foreach { orb =>
if (!orb.collected && distance(playerX, playerZ, orb.x, orb.z) < 62.0) {
orb.collected = true; collectedOrbs += 1; health = math.min(100, health + 7)
showMessage("Bloom Orb collected! +7 health (" + collectedOrbs + "/" + requiredOrbs + ")", 2.0)
}
}
enemies.foreach { enemy =>
if (enemy.hp > 0) {
val ex = playerX - enemy.x; val ez = playerZ - enemy.z
val d = math.sqrt(ex * ex + ez * ez)
if (d < 560.0 && d > 48.0) {
val speed = (42.0 + currentLevel * 2.6) * (if (enemy.boss) 0.82 else 1.0)
enemy.x += ex / d * speed * dt
enemy.z += ez / d * speed * dt
} else if (d <= 52.0 && enemy.attackCooldown <= 0.0) {
if (hurtCooldown <= 0.0) {
val reduction = math.min(0.65, currentArmorReduction + currentShieldReduction + (if (shieldTime > 0.0) 0.55 else 0.0))
val hit = math.max(1, (enemy.damage * (1.0 - reduction)).toInt)
health -= hit; hurtCooldown = 0.8
showMessage("Guardian touched you! Damage: " + hit + ". Move and use your power!", 1.5)
}
enemy.attackCooldown = 1.0
}
if (enemy.attackCooldown > 0.0) enemy.attackCooldown -= dt
}
}
if (health <= 0) {
health = 0; gameOver = true; hasMoveTarget = false
showMessage("Your hero needs a retry. Press R to restart this level.", 100.0)
}
if (objectiveComplete && playerZ >= 930.0 && !rewardGiven) completeCurrentLevel()
networkSend()
}
private def damageNearest(radius: Double, damage: Int): Int = {
val candidates = enemies.filter(_.hp > 0).map(e => (e, distance(playerX, playerZ, e.x, e.z)))
.filter(_._2 <= radius).sortBy(_._2)
candidates.headOption match {
case Some((enemy, _)) => enemy.hp = math.max(0, enemy.hp - damage); if (enemy.hp == 0) kills += 1; 1
case None => 0
}
}
private def basicAttack(): Unit = {
if (paused || gameOver || levelComplete) return
if (attackCooldown > 0.0) return
attackCooldown = 0.42; attackFlash = 0.25
val hit = damageNearest(165.0, 1)
if (hit == 0) showMessage("No guardian in range ? move closer!", 1.0)
else showMessage("Nature strike!", 0.7)
}
private def activatePower(): Unit = {
if (paused || gameOver || levelComplete) return
if (powerCooldown > 0.0) { showMessage("Power recharging: " + powerCooldown.toInt + "s", 1.0); return }
val defn = powers.find(_.name == equippedPower).getOrElse(powers(0))
if (!ownedPowers.contains(defn.name)) { showMessage("Buy and equip this power in the shop first.", 2.0); return }
powerCooldown = 8.0
def areaDamage(radius: Double, dmg: Int): Int = {
var count = 0
enemies.foreach { enemy => if (enemy.hp > 0 && distance(playerX, playerZ, enemy.x, enemy.z) <= radius) {
enemy.hp = math.max(0, enemy.hp - dmg); if (enemy.hp == 0) kills += 1; count += 1
}}
count
}
defn.effect match {
case "pulse" => val n = areaDamage(230.0, 2); showMessage("NATURE PULSE! Hits: " + n, 2.0)
case "shield" => shieldTime = 6.0; showMessage("THORN SHIELD active!", 2.0)
case "dash" => dashTime = 4.5; showMessage("BLOOM DASH! Move quickly!", 2.0)
case "heal" => health = math.min(100, health + 38); showMessage("HEALING BLOOM! Health restored.", 2.0)
case "storm" => val n = areaDamage(440.0, 3); showMessage("STORM VINES! Hits: " + n, 2.0)
case "sun" => val n = damageNearest(620.0, 5); showMessage("SUN BURST! Hits: " + n, 2.0)
case "crystal" => shieldTime = 10.0; showMessage("CRYSTAL GUARD active!", 2.0)
case "starfall" => val n = areaDamage(580.0, 4); showMessage("STARFALL BLOOM! Hits: " + n, 2.0)
case "phoenix" => health = math.min(100, health + 55); shieldTime = 5.0; showMessage("PHOENIX BLOOM! Heal + shield.", 2.0)
case "nova" => val n = areaDamage(700.0, 6); showMessage("NATURE NOVA! Hits: " + n, 2.0)
case "worldtree" => health = 100; shieldTime = 8.0; val n = areaDamage(900.0, 8); showMessage("WORLD TREE ULTIMATE! Hits: " + n, 3.0)
case _ => showMessage("Power ready!", 1.0)
}
}
private def completeCurrentLevel(): Unit = {
if (rewardGiven) return
rewardGiven = true; levelComplete = true; hasMoveTarget = false
val reward = currentLevel * 10 + collectedOrbs * 5
coins += reward
if (currentLevel < 20) highestUnlocked = math.max(highestUnlocked, currentLevel + 1)
saveProgress()
if (nextButton != null) nextButton.setEnabled(true)
showMessage("LEVEL " + currentLevel + " COMPLETE! +" + reward + " COINS", 100.0)
if (currentLevel == 20) {
JOptionPane.showMessageDialog(frame,
"THE FUTURE BLOOM IS RESTORED!\n\nYou completed all 20 levels and saved Natureverse.\nCoins earned this level: " + reward,
"EVENT 5 COMPLETE", JOptionPane.INFORMATION_MESSAGE)
}
}
private def nextLevel(): Unit = {
if (!levelComplete) {
showMessage("Finish the mission first. Then press ENTER for the next level.", 2.5)
return
}
if (currentLevel >= 20) {
stopGameTimer()
refreshLevelsCard()
screen("levels")
} else {
startLevel(currentLevel + 1)
screen("game")
if (gamePanel != null) gamePanel.requestFocusInWindow()
}
}
// B returns to the level selector from the adventure. Leaving an unfinished
// level does not award coins; selecting a level again starts it from the beginning.
private def returnToLevels(): Unit = {
stopGameTimer()
paused = true
refreshLevelsCard()
screen("levels")
}
private def stopGameTimer(): Unit = if (gameTimer != null) gameTimer.stop()
private class GamePanel extends JPanel {
setFocusable(true)
setBackground(Color.BLACK)
addMouseListener(new MouseAdapter {
override def mouseClicked(e: MouseEvent): Unit = {
requestFocusInWindow()
if (!paused && !gameOver && !levelComplete && e.getY > getHeight * 0.39) {
val horizon = getHeight * 0.36
val deltaY = math.max(18.0, e.getY - horizon)
val depth = 56000.0 / deltaY
val relZ = depth - 250.0
val scale = 260.0 / math.max(100.0, depth)
val worldX = playerX + (e.getX - getWidth / 2.0) / (scale * 1.7)
targetX = clamp(worldX, -460.0, 460.0)
targetZ = clamp(playerZ + relZ, -40.0, 1010.0)
hasMoveTarget = true
}
}
})
bind("W", "UP", "S", "DOWN", "A", "LEFT", "D", "RIGHT")
bindAction("SPACE") { basicAttack() }
bindAction("Q") { activatePower() }
bindAction("P") { if (!levelComplete && !gameOver) paused = !paused }
bindAction("R") { startLevel(currentLevel) }
bindAction("ENTER") { if (levelComplete) nextLevel() }
bindAction("B") { returnToLevels() }
private def bind(keys: String*): Unit = keys.foreach { key =>
val im = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
val am = getActionMap
im.put(KeyStroke.getKeyStroke("pressed " + key), "press_" + key)
im.put(KeyStroke.getKeyStroke("released " + key), "release_" + key)
am.put("press_" + key, new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = pressedKeys += key })
am.put("release_" + key, new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = pressedKeys -= key })
}
private def bindAction(key: String)(action: => Unit): Unit = {
getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("pressed " + key), "action_" + key)
getActionMap.put("action_" + key, new AbstractAction { override def actionPerformed(e: ActionEvent): Unit = action })
}
override def paintComponent(graphics: Graphics): Unit = {
super.paintComponent(graphics)
val g = graphics.create().asInstanceOf[Graphics2D]
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
drawWorld(g, getWidth, getHeight)
g.dispose()
}
}
private def project(wx: Double, wz: Double, w: Int, h: Int): Option[ScreenPoint] = {
val relative = wz - playerZ
val depth = relative + 250.0
if (depth < 95.0 || relative > 1700.0 || relative < -500.0) None
else {
val scale = 260.0 / depth
val sx = w / 2.0 + (wx - playerX) * scale * 1.7
val sy = h * 0.36 + 56000.0 / depth
Some(ScreenPoint(sx.toInt, sy.toInt, scale, depth))
}
}
private def drawWorld(g: Graphics2D, w: Int, h: Int): Unit = {
g.setPaint(new GradientPaint(0f, 0f, skyDark, 0f, (h * 0.58).toFloat, skyLight))
g.fillRect(0, 0, w, h)
g.setColor(new Color(255, 225, 153, 190)); g.fillOval((w * 0.76).toInt, (h * 0.12).toInt, 88, 88)
val mountain1 = new Polygon(Array(0, (w * 0.13).toInt, (w * 0.26).toInt, (w * 0.43).toInt, (w * 0.59).toInt, (w * 0.77).toInt, w),
Array((h * 0.48).toInt, (h * 0.31).toInt, (h * 0.47).toInt, (h * 0.29).toInt, (h * 0.47).toInt, (h * 0.32).toInt, (h * 0.48).toInt), 7)
g.setColor(new Color(25, 79, 93)); g.fillPolygon(mountain1)
val horizon = (h * 0.38).toInt
g.setPaint(new GradientPaint(0f, horizon.toFloat, new Color(28, 135, 91), 0f, h.toFloat, deepGreen))
g.fillRect(0, horizon, w, h - horizon)
val path = new Polygon(Array((w * 0.46).toInt, (w * 0.54).toInt, (w * 0.9).toInt, (w * 0.1).toInt),
Array(horizon, horizon, h, h), 4)
g.setColor(new Color(115, 111, 76)); g.fillPolygon(path)
g.setColor(new Color(228, 212, 148, 115)); g.setStroke(new BasicStroke(3f))
g.drawLine((w * 0.46).toInt, horizon, (w * 0.1).toInt, h)
g.drawLine((w * 0.54).toInt, horizon, (w * 0.9).toInt, h)
var i = 0
while (i < 12) {
val z = -200.0 + i * 135.0
val side = if (i % 2 == 0) -1.0 else 1.0
val x = side * (365.0 + (i % 3) * 65.0)
project(x, z + playerZ, w, h).foreach(p => drawTree(g, p, w))
i += 1
}
project(0.0, 990.0, w, h).foreach(p => drawPortal(g, p, objectiveComplete))
orbs.filterNot(_.collected).sortBy(o => -o.z).foreach(o => project(o.x, o.z, w, h).foreach(p => drawOrb(g, p)))
enemies.filter(_.hp > 0).sortBy(e => -e.z).foreach(e => project(e.x, e.z, w, h).foreach(p => drawEnemy(g, p, e)))
remotePlayer.filter(_.level == currentLevel).foreach { remote =>
project(remote.x, remote.z, w, h).foreach(p => drawRemoteHero(g, p, remote))
}
drawHero(g, w, h)
drawHud(g, w, h)
if (paused || levelComplete || gameOver) drawOverlay(g, w, h)
}
private def drawTree(g: Graphics2D, p: ScreenPoint, w: Int): Unit = {
if (p.x < -100 || p.x > w + 100) return
val s = clamp(p.scale, 0.12, 1.05)
val trunkH = math.max(5, (80 * s).toInt)
val crown = math.max(10, (62 * s).toInt)
g.setColor(new Color(48, 48, 34)); g.fillRoundRect(p.x - (8 * s).toInt, p.y - trunkH, math.max(3, (16 * s).toInt), trunkH, 5, 5)
g.setColor(new Color(13, 91, 65)); g.fillOval(p.x - crown / 2, p.y - trunkH - crown / 2, crown, crown)
g.setColor(new Color(39, 174, 105)); g.fillOval(p.x - crown / 2, p.y - trunkH - crown, crown, crown)
g.setColor(new Color(142, 248, 166, 175)); g.fillOval(p.x - crown / 5, p.y - trunkH - crown + crown / 5, math.max(3, crown / 3), math.max(3, crown / 3))
}
private def drawOrb(g: Graphics2D, p: ScreenPoint): Unit = {
val r = math.max(5, (17 * clamp(p.scale, 0.18, 1.0)).toInt)
g.setColor(new Color(255, 215, 72, 65)); g.fillOval(p.x - r * 2, p.y - r * 2, r * 4, r * 4)
g.setColor(gold); g.fillOval(p.x - r, p.y - r, r * 2, r * 2)
g.setColor(Color.WHITE); g.fillOval(p.x - r / 3, p.y - r / 2, math.max(2, r / 2), math.max(2, r / 2))
}
private def drawEnemy(g: Graphics2D, p: ScreenPoint, e: Enemy): Unit = {
val s = clamp(p.scale * (if (e.boss) 1.7 else 1.0), 0.2, 1.4)
val bw = math.max(16, (39 * s).toInt); val bh = math.max(22, (66 * s).toInt); val head = math.max(12, (25 * s).toInt)
g.setColor(new Color(0, 0, 0, 85)); g.fillOval(p.x - bw, p.y - 5, bw * 2, math.max(5, bw / 2))
g.setColor(if (e.boss) new Color(81, 43, 94) else new Color(38, 49, 52))
g.fillRoundRect(p.x - bw / 2, p.y - bh, bw, bh, 12, 12)
g.setColor(if (e.boss) new Color(158, 77, 183) else new Color(102, 49, 84))
g.fillOval(p.x - head / 2, p.y - bh - head + 5, head, head)
g.setColor(new Color(255, 74, 105)); g.fillOval(p.x - head / 3, p.y - bh - head / 2, math.max(3, head / 5), math.max(3, head / 5))
g.fillOval(p.x + head / 8, p.y - bh - head / 2, math.max(3, head / 5), math.max(3, head / 5))
val barW = math.max(24, (54 * s).toInt); val barY = p.y - bh - head - 10
g.setColor(new Color(15, 15, 18, 220)); g.fillRoundRect(p.x - barW / 2, barY, barW, 6, 5, 5)
g.setColor(new Color(255, 75, 106)); g.fillRoundRect(p.x - barW / 2, barY, (barW.toDouble * e.hp / e.maxHp).toInt, 6, 5, 5)
if (e.boss) { g.setFont(new Font("SansSerif", Font.BOLD, math.max(9, (12 * s).toInt))); g.setColor(gold); g.drawString("BOSS", p.x - 18, barY - 5) }
}
private def drawPortal(g: Graphics2D, p: ScreenPoint, unlocked: Boolean): Unit = {
val s = clamp(p.scale, 0.2, 1.1); val rw = math.max(24, (78 * s).toInt); val rh = math.max(38, (126 * s).toInt)
g.setColor(new Color(0, 0, 0, 70)); g.fillOval(p.x - rw, p.y - 5, rw * 2, 16)
g.setColor(if (unlocked) new Color(65, 248, 216, 90) else new Color(120, 123, 149, 60)); g.fillOval(p.x - rw, p.y - rh, rw * 2, rh * 2)
g.setStroke(new BasicStroke(math.max(3f, (7 * s).toFloat)))
g.setColor(if (unlocked) mint else new Color(130, 135, 155)); g.drawOval(p.x - rw, p.y - rh, rw * 2, rh * 2)
g.setFont(new Font("SansSerif", Font.BOLD, math.max(9, (12 * s).toInt))); g.setColor(Color.WHITE)
g.drawString(if (unlocked) "OPEN" else "LOCKED", p.x - rw, p.y - rh - 7)
}
private def drawHero(g: Graphics2D, w: Int, h: Int): Unit = {
val x = w / 2; val y = (h * 0.84).toInt
g.setColor(new Color(0, 0, 0, 95)); g.fillOval(x - 38, y - 4, 76, 18)
val body = avatarIndex match {
case 1 => new Color(113, 83, 174)
case 2 => new Color(75, 145, 184)
case 3 => new Color(185, 131, 45)
case _ => new Color(42, 145, 112)
}
if (equippedArmor != "No Armor") {
g.setColor(new Color(214, 224, 227)); g.fillRoundRect(x - 25, y - 87, 50, 68, 16, 16)
g.setColor(new Color(78, 101, 119)); g.drawRoundRect(x - 25, y - 87, 50, 68, 16, 16)
}
g.setColor(body); g.fillRoundRect(x - 22, y - 77, 44, 66, 14, 14)
g.setColor(new Color(239, 188, 145)); g.fillOval(x - 16, y - 113, 32, 34)
g.setColor(new Color(31, 42, 45)); g.fillArc(x - 17, y - 119, 34, 29, 0, 180)
g.setColor(new Color(29, 47, 55)); g.fillRect(x - 18, y - 17, 14, 18); g.fillRect(x + 4, y - 17, 14, 18)
g.setColor(mint); g.fillOval(x + 25, y - 88, 19, 19); g.setColor(Color.WHITE); g.drawOval(x + 25, y - 88, 19, 19)
if (equippedShield != "No Shield" || shieldTime > 0.0) {
g.setColor(new Color(107, 229, 255, 120)); g.setStroke(new BasicStroke(4f)); g.drawOval(x - 43, y - 129, 86, 140)
}
if (attackFlash > 0.0) {
g.setColor(new Color(157, 255, 232, 170)); g.setStroke(new BasicStroke(8f)); g.drawArc(x - 22, y - 133, 116, 105, 205, 115)
}
}
private def drawRemoteHero(g: Graphics2D, p: ScreenPoint, r: RemoteSnapshot): Unit = {
val s = clamp(p.scale, 0.2, 1.0); val bw = math.max(10, (29 * s).toInt); val bh = math.max(20, (57 * s).toInt)
val color = r.avatar match { case 1 => new Color(144, 94, 199); case 2 => new Color(82, 157, 207); case 3 => new Color(207, 151, 65); case _ => new Color(53, 178, 134) }
g.setColor(new Color(0, 0, 0, 75)); g.fillOval(p.x - bw, p.y - 3, bw * 2, 9)
g.setColor(color); g.fillRoundRect(p.x - bw / 2, p.y - bh, bw, bh, 8, 8)
g.setColor(new Color(238, 190, 148)); g.fillOval(p.x - bw / 3, p.y - bh - bw, (bw * 2 / 3).max(6), (bw * 2 / 3).max(6))
g.setFont(new Font("SansSerif", Font.BOLD, 10)); g.setColor(Color.WHITE); g.drawString(r.name, p.x - 25, p.y - bh - 20)
}
private def drawHud(g: Graphics2D, w: Int, h: Int): Unit = {
g.setColor(new Color(4, 18, 28, 218)); g.fillRoundRect(16, 14, 335, 125, 20, 20)
g.setColor(mint); g.setStroke(new BasicStroke(2f)); g.drawRoundRect(16, 14, 335, 125, 20, 20)
g.setFont(new Font("SansSerif", Font.BOLD, 20)); g.setColor(new Color(181, 255, 226)); g.drawString("NATUREVERSE", 34, 42)
g.setFont(new Font("SansSerif", Font.PLAIN, 12)); g.setColor(Color.WHITE); g.drawString("LEVEL " + currentLevel + " / 20 ? " + networkLabel, 35, 62)
g.setColor(new Color(30, 45, 48)); g.fillRoundRect(35, 75, 210, 13, 7, 7)
g.setColor(if (health > 30) new Color(63, 220, 139) else new Color(255, 83, 96)); g.fillRoundRect(35, 75, (210.0 * health / 100.0).toInt, 13, 7, 7)
g.setFont(new Font("SansSerif", Font.BOLD, 11)); g.setColor(Color.WHITE); g.drawString("HP " + health + "%", 255, 86)
g.setColor(gold); g.drawString("COINS " + coins, 35, 113)
g.setColor(new Color(171, 248, 217)); g.drawString("ORBS " + collectedOrbs + "/" + requiredOrbs + " GUARDIANS " + kills + "/" + enemies.length, 133, 113)
val powerX = w - 300
g.setColor(new Color(4, 18, 28, 210)); g.fillRoundRect(powerX, 14, 284, 105, 18, 18)
g.setColor(new Color(255, 218, 104, 150)); g.drawRoundRect(powerX, 14, 284, 105, 18, 18)
g.setFont(new Font("SansSerif", Font.BOLD, 14)); g.setColor(gold); g.drawString("EQUIPPED POWER", powerX + 15, 39)
g.setFont(new Font("SansSerif", Font.BOLD, 15)); g.setColor(Color.WHITE); g.drawString(equippedPower, powerX + 15, 63)
g.setFont(new Font("SansSerif", Font.PLAIN, 12)); g.setColor(mint)
g.drawString(if (powerCooldown <= 0.0) "Q READY" else "Q RECHARGING " + powerCooldown.toInt + "s", powerX + 15, 88)
g.setFont(new Font("SansSerif", Font.PLAIN, 12)); g.setColor(Color.WHITE)
val controls = "CLICK / WASD MOVE SPACE ATTACK Q POWER P PAUSE R RESTART"
val tw = g.getFontMetrics.stringWidth(controls)
g.setColor(new Color(4, 16, 24, 205)); g.fillRoundRect((w - tw - 32) / 2, h - 40, tw + 32, 27, 14, 14)
g.setColor(Color.WHITE); g.drawString(controls, (w - tw) / 2, h - 22)
if (messageTime > 0.0 && !paused && !levelComplete && !gameOver) {
val mw = math.min(w - 40, math.max(360, message.length * 7 + 35))
g.setColor(new Color(4, 20, 29, 215)); g.fillRoundRect((w - mw) / 2, h - 86, mw, 34, 17, 17)
g.setColor(mint); g.drawRoundRect((w - mw) / 2, h - 86, mw, 34, 17, 17)
g.setFont(new Font("SansSerif", Font.BOLD, 13)); g.setColor(Color.WHITE); g.drawString(message, (w - mw) / 2 + 15, h - 64)
}
}
private def drawOverlay(g: Graphics2D, w: Int, h: Int): Unit = {
g.setColor(new Color(0, 8, 18, 195)); g.fillRect(0, 0, w, h)
val bw = math.min(660, w - 50); val bh = 250; val x = (w - bw) / 2; val y = (h - bh) / 2
g.setColor(new Color(6, 34, 45, 245)); g.fillRoundRect(x, y, bw, bh, 28, 28)
g.setColor(mint); g.setStroke(new BasicStroke(3f)); g.drawRoundRect(x, y, bw, bh, 28, 28)
val title = if (levelComplete) (if (currentLevel == 20) "EVENT 5 COMPLETE!" else "LEVEL " + currentLevel + " COMPLETE!") else if (gameOver) "HERO NEEDS A RETRY" else "GAME PAUSED"
g.setFont(new Font("SansSerif", Font.BOLD, 29)); g.setColor(if (gameOver) new Color(255, 112, 125) else gold)
g.drawString(title, w / 2 - g.getFontMetrics.stringWidth(title) / 2, y + 65)
g.setFont(new Font("SansSerif", Font.PLAIN, 16)); g.setColor(Color.WHITE)
val detail = if (levelComplete) "Press ENTER for the next level, or B to return to level selection." else if (gameOver) "Press R to retry this level, or B to return to level selection." else "Press P to resume, or B to return to level selection."
g.drawString(detail, w / 2 - g.getFontMetrics.stringWidth(detail) / 2, y + 112)
g.setFont(new Font("SansSerif", Font.BOLD, 16)); g.setColor(gold)
val stats = "Bloom Orbs: " + collectedOrbs + "/" + requiredOrbs + " Guardians defeated: " + kills + "/" + enemies.length
g.drawString(stats, w / 2 - g.getFontMetrics.stringWidth(stats) / 2, y + 153)
g.setFont(new Font("SansSerif", Font.PLAIN, 14)); g.setColor(mint)
val bottom = if (levelComplete) "ENTER = NEXT LEVEL ? B = BACK TO LEVELS" else if (gameOver) "R = restart ? B = back to levels" else "P = resume ? B = back to levels"
g.drawString(bottom, w / 2 - g.getFontMetrics.stringWidth(bottom) / 2, y + 197)
}
private class LanLink(socket: Socket) {
private val reader = new BufferedReader(new InputStreamReader(socket.getInputStream, "UTF-8"))
private val writer = new PrintWriter(socket.getOutputStream, true)
@volatile private var remote: RemoteSnapshot = null
@volatile private var closed = false
private val readThread = new Thread(new Runnable {
override def run(): Unit = {
try {
var line: String = null
while (!closed && { line = reader.readLine(); line != null }) {
parseSnapshot(line).foreach(s => remote = s)
}
} catch { case _: Throwable => () }
finally { close() }
}
}, "Natureverse-LAN-Reader")
readThread.setDaemon(true); readThread.start()
def send(line: String): Unit = synchronized {
if (!closed) { writer.println(line); if (writer.checkError()) close() }
}
def latest: Option[RemoteSnapshot] = Option(remote)
def close(): Unit = {
if (!closed) {
closed = true
try { socket.close() } catch { case _: Throwable => () }
}
}
}
private class HostServer(port: Int) {
private val serverSocket = new ServerSocket(port)
@volatile private var peer: LanLink = null
private val acceptThread = new Thread(new Runnable {
override def run(): Unit = {
try { peer = new LanLink(serverSocket.accept()) }
catch { case _: Throwable => () }
}
}, "Natureverse-LAN-Accept")
acceptThread.setDaemon(true); acceptThread.start()
def send(line: String): Unit = { val p = peer; if (p != null) p.send(line) }
def latest: Option[RemoteSnapshot] = { val p = peer; if (p == null) None else p.latest }
def close(): Unit = {
try { serverSocket.close() } catch { case _: Throwable => () }
val p = peer; if (p != null) p.close()
}
}
private def parseSnapshot(line: String): Option[RemoteSnapshot] = {
try {
val p = line.split("\\|", 6)
if (p.length < 6) None
else Some(RemoteSnapshot(p(0).take(18), p(1).toInt, p(2).toDouble, p(3).toDouble,
math.max(0, math.min(100, p(4).toInt)), math.max(0, math.min(3, p(5).toInt))))
} catch { case _: Throwable => None }
}
def run(): Unit = main(Array[String]())
def main(args: Array[String]): Unit = {
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
try { launch() }
catch {
case e: Throwable =>
e.printStackTrace()
JOptionPane.showMessageDialog(null, "Natureverse startup error:\n" + e.toString, "Startup Error", JOptionPane.ERROR_MESSAGE)
}
}
})
}
}
// Kojo: run the WHOLE script. This final Scala statement opens the Swing window.
BixuuSpringSeasonEvent5.main(Array[String]())