Code Sketch
Bixuu02
Category: Programming
import java.awt.{AlphaComposite, BasicStroke, Color, Dimension, Font, Graphics, Graphics2D, GradientPaint, Polygon, RenderingHints, Cursor}
import java.awt.event.{ActionEvent, ActionListener, KeyAdapter, KeyEvent, MouseAdapter, MouseEvent}
import javax.swing.{JFrame, JPanel, SwingUtilities, Timer, WindowConstants}
import java.util.Random
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
// ================================================================
// BIXUU - SPRING SEASON EVENT 02
// AURORA FALLS: THE LOST SPRING
//
// Original animated Scala + Java Swing exploration game.
// No external images, libraries or internet required.
//
// CONTROLS:
// WASD / ARROWS = MOVE
// E = COLLECT CRYSTAL / OPEN PORTAL
// SPACE = DASH
// F = TOGGLE GLOW
// M = TOGGLE MAP
// ESC = PAUSE
// R = RESTART
// ================================================================
object BixuuAuroraFalls {
val APP_TITLE = "BIXUU - SPRING SEASON EVENT 02: AURORA FALLS"
val WORLD_W = 1900
val WORLD_H = 1350
val SCREEN_W = 1180
val SCREEN_H = 760
val GOAL_COUNT = 6
val random = new Random(20261031L)
val BG_TOP = new Color(9, 14, 39)
val BG_BOTTOM = new Color(15, 64, 70)
val DEEP_GREEN = new Color(12, 49, 43)
val GREEN = new Color(41, 135, 76)
val MINT = new Color(133, 255, 201)
val CYAN = new Color(73, 239, 240)
val PINK = new Color(255, 125, 203)
val PURPLE = new Color(174, 117, 255)
val GOLD = new Color(255, 224, 132)
val WHITE = new Color(245, 252, 255)
final class Crystal(
val x: Double,
val y: Double,
val hue: Int,
val label: String
) {
var collected = false
}
final class GardenTree(
val x: Double,
val y: Double,
val scale: Double,
val hue: Int
)
final class GardenFlower(
val x: Double,
val y: Double,
val size: Double,
val hue: Int,
val phase: Double,
val special: Boolean
)
final class Star(
val x: Double,
val y: Double,
val size: Int,
val phase: Double
)
final class Particle(
var x: Double,
var y: Double,
var vx: Double,
var vy: Double,
val size: Double,
val hue: Int,
var life: Double
)
val keys = mutable.Set[Int]()
val crystals = ArrayBuffer[Crystal]()
val trees = ArrayBuffer[GardenTree]()
val flowers = ArrayBuffer[GardenFlower]()
val stars = ArrayBuffer[Star]()
val particles = ArrayBuffer[Particle]()
var playerX = 300.0
var playerY = 1110.0
var facingX = 0.0
var facingY = -1.0
var cameraX = 0.0
var cameraY = 0.0
var frames = 0L
var remainingSeconds = 360.0
var started = false
var paused = false
var won = false
var lost = false
var glowMode = true
var showMap = true
var dashTime = 0.0
var dashCooldown = 0.0
var toast = "Welcome to Aurora Falls!"
var toastTime = 4.0
var glitchTime = 0.0
var glitchActive = false
var panel: GamePanel = null
var window: JFrame = null
var gameTimer: Timer = null
val portalX = 1530.0
val portalY = 560.0
// --------------------------------------------------------------
// HELPER METHODS
// --------------------------------------------------------------
def clamp(
value: Double,
minimum: Double,
maximum: Double
): Double = {
math.max(minimum, math.min(maximum, value))
}
def distance(
x1: Double,
y1: Double,
x2: Double,
y2: Double
): Double = {
val dx = x1 - x2
val dy = y1 - y2
math.sqrt(dx * dx + dy * dy)
}
def hueColor(
hue: Int,
saturation: Float = 0.82f,
brightness: Float = 1.0f
): Color = {
Color.getHSBColor(
(hue.toDouble / 360.0).toFloat,
saturation,
brightness
)
}
def showToast(message: String, duration: Double = 2.8): Unit = {
toast = message
toastTime = duration
}
def collectedCount: Int = {
crystals.count(_.collected)
}
def screenX(x: Double): Int = {
math.round(x - cameraX).toInt
}
def screenY(y: Double): Int = {
math.round(y - cameraY).toInt
}
// --------------------------------------------------------------
// WORLD GENERATION
// --------------------------------------------------------------
def setupWorld(): Unit = {
crystals.clear()
trees.clear()
flowers.clear()
stars.clear()
particles.clear()
// Six magical crystals
crystals += new Crystal(300, 900, 155, "MINT SEED")
crystals += new Crystal(590, 700, 190, "STAR PETAL")
crystals += new Crystal(980, 920, 285, "MOON BLOOM")
crystals += new Crystal(1330, 920, 48, "SUN CRYSTAL")
crystals += new Crystal(1640, 760, 195, "WATER GEM")
crystals += new Crystal(1040, 345, 315, "AURORA ORB")
// Garden flower regions
val zones = Array(
(100.0, 150.0, 570.0, 300.0),
(1200.0, 130.0, 560.0, 320.0),
(100.0, 950.0, 650.0, 260.0),
(1100.0, 1000.0, 650.0, 250.0),
(650.0, 400.0, 300.0, 250.0),
(1450.0, 520.0, 280.0, 280.0)
)
val flowerHues = Array(145, 164, 188, 282, 315, 48)
var i = 0
while (i < 220) {
val z = zones(random.nextInt(zones.length))
val x = z._1 + random.nextDouble() * z._3
val y = z._2 + random.nextDouble() * z._4
val hue = flowerHues(
random.nextInt(flowerHues.length)
)
flowers += new GardenFlower(
x,
y,
5.0 + random.nextDouble() * 8.0,
hue,
random.nextDouble() * 6.28,
random.nextInt(8) == 0
)
i += 1
}
// Trees around the forest
val treeData = Array(
(120.0, 250.0, 1.1, 150),
(340.0, 190.0, 0.9, 160),
(670.0, 220.0, 1.0, 145),
(1050.0, 180.0, 0.9, 155),
(1740.0, 220.0, 1.1, 165),
(1790.0, 620.0, 1.0, 150),
(1680.0, 1100.0, 1.1, 160),
(1270.0, 1190.0, 0.9, 145),
(810.0, 1190.0, 1.0, 165),
(360.0, 1200.0, 1.1, 155),
(110.0, 820.0, 0.9, 160),
(490.0, 520.0, 0.8, 150),
(970.0, 700.0, 0.9, 165),
(1320.0, 370.0, 0.8, 150)
)
treeData.foreach { t =>
trees += new GardenTree(
t._1, t._2, t._3, t._4
)
}
// Cosmic stars
i = 0
while (i < 180) {
stars += new Star(
random.nextDouble() * WORLD_W,
random.nextDouble() * WORLD_H,
1 + random.nextInt(3),
random.nextDouble() * 6.28
)
i += 1
}
// Ambient particles
i = 0
while (i < 100) {
particles += new Particle(
random.nextDouble() * WORLD_W,
random.nextDouble() * WORLD_H,
(random.nextDouble() - 0.5) * 8.0,
-8.0 - random.nextDouble() * 18.0,
2.0 + random.nextDouble() * 3.0,
flowerHues(random.nextInt(flowerHues.length)),
3.0 + random.nextDouble() * 7.0
)
i += 1
}
}
// --------------------------------------------------------------
// GAME STATE
// --------------------------------------------------------------
def startAdventure(): Unit = {
if (!started && !won && !lost) {
started = true
paused = false
showToast(
"Adventure started! Find all six magical crystals.",
4.0
)
}
if (panel != null) {
panel.requestFocusInWindow()
}
}
def resetGame(): Unit = {
playerX = 300.0
playerY = 1110.0
facingX = 0.0
facingY = -1.0
frames = 0L
remainingSeconds = 360.0
started = false
paused = false
won = false
lost = false
glowMode = true
showMap = true
dashTime = 0.0
dashCooldown = 0.0
crystals.foreach(_.collected = false)
keys.clear()
showToast(
"BIXUU - SPRING SEASON EVENT 02",
4.0
)
if (panel != null) {
panel.requestFocusInWindow()
panel.repaint()
}
}
def triggerDash(): Unit = {
if (
started &&
!paused &&
!won &&
!lost &&
dashCooldown <= 0.0
) {
dashTime = 0.22
dashCooldown = 1.0
showToast("COSMIC DASH!", 0.7)
}
}
// --------------------------------------------------------------
// COLLECT CRYSTALS AND OPEN PORTAL
// --------------------------------------------------------------
def spawnBurst(
x: Double,
y: Double,
hue: Int,
amount: Int
): Unit = {
var i = 0
while (i < amount) {
val angle = random.nextDouble() * math.Pi * 2.0
val speed = 35.0 + random.nextDouble() * 135.0
particles += new Particle(
x,
y,
math.cos(angle) * speed,
math.sin(angle) * speed,
2.0 + random.nextDouble() * 4.0,
hue,
0.7 + random.nextDouble() * 1.1
)
i += 1
}
}
def interact(): Unit = {
if (!started || paused || won || lost) {
return
}
val nearby = crystals
.filter(c => !c.collected)
.sortBy(c => distance(playerX, playerY, c.x, c.y))
.headOption
nearby match {
case Some(c)
if distance(playerX, playerY, c.x, c.y) < 100.0 =>
c.collected = true
showToast(
"CRYSTAL RESTORED: " +
c.label +
" [" + collectedCount + "/" + GOAL_COUNT + "]",
2.7
)
spawnBurst(c.x, c.y, c.hue, 28)
if (collectedCount == GOAL_COUNT) {
showToast(
"ALL CRYSTALS RESTORED! FIND THE WATERFALL PORTAL!",
4.0
)
spawnBurst(portalX, portalY, 160, 55)
}
case _ =>
if (
distance(playerX, playerY, portalX, portalY) < 125.0
) {
if (collectedCount >= GOAL_COUNT) {
won = true
showToast(
"AURORA FALLS RESTORED! ADVENTURE COMPLETE!",
9.0
)
spawnBurst(portalX, portalY, 165, 100)
} else {
showToast(
"PORTAL LOCKED! COLLECT ALL SIX CRYSTALS.",
2.8
)
}
} else {
showToast(
"MOVE CLOSE TO A CRYSTAL OR THE PORTAL AND PRESS E.",
2.5
)
}
}
}
// --------------------------------------------------------------
// GAME UPDATE AND ANIMATION
// --------------------------------------------------------------
def updateGame(dt: Double): Unit = {
frames += 1
if (toastTime > 0.0) {
toastTime = math.max(0.0, toastTime - dt)
}
if (dashTime > 0.0) {
dashTime = math.max(0.0, dashTime - dt)
}
if (dashCooldown > 0.0) {
dashCooldown = math.max(0.0, dashCooldown - dt)
}
glitchTime -= dt
if (glitchTime <= 0.0) {
glitchTime = 2.0 + random.nextDouble() * 3.0
glitchActive = random.nextInt(5) == 0
}
// Update floating particles
particles.foreach { p =>
p.x += p.vx * dt
p.y += p.vy * dt
p.life -= dt
if (p.y < 0) {
p.y = WORLD_H
}
if (p.x < 0) {
p.x = WORLD_W
}
if (p.x > WORLD_W) {
p.x = 0
}
}
val deadParticles = particles.filter(_.life <= 0.0)
deadParticles.foreach { p =>
particles -= p
}
while (particles.length < 95) {
particles += new Particle(
random.nextDouble() * WORLD_W,
WORLD_H - 10,
(random.nextDouble() - 0.5) * 14.0,
-8.0 - random.nextDouble() * 20.0,
2.0 + random.nextDouble() * 3.0,
Array(150, 165, 188, 285, 315, 48)(
random.nextInt(6)
),
4.0
)
}
if (!started || paused || won || lost) {
return
}
remainingSeconds = math.max(
0.0,
remainingSeconds - dt
)
if (remainingSeconds <= 0.0) {
lost = true
showToast(
"TIME EXPIRED! PRESS R TO TRY AGAIN.",
10.0
)
return
}
// Movement input
var mx = 0.0
var my = 0.0
if (
keys.contains(KeyEvent.VK_W) ||
keys.contains(KeyEvent.VK_UP)
) {
my -= 1.0
}
if (
keys.contains(KeyEvent.VK_S) ||
keys.contains(KeyEvent.VK_DOWN)
) {
my += 1.0
}
if (
keys.contains(KeyEvent.VK_A) ||
keys.contains(KeyEvent.VK_LEFT)
) {
mx -= 1.0
}
if (
keys.contains(KeyEvent.VK_D) ||
keys.contains(KeyEvent.VK_RIGHT)
) {
mx += 1.0
}
val length = math.sqrt(mx * mx + my * my)
if (length > 0.0) {
mx /= length
my /= length
facingX = mx
facingY = my
val speed =
if (dashTime > 0.0) 430.0
else 215.0
playerX = clamp(
playerX + mx * speed * dt,
45.0,
WORLD_W - 45.0
)
playerY = clamp(
playerY + my * speed * dt,
45.0,
WORLD_H - 45.0
)
if (dashTime > 0.0 && frames % 3 == 0) {
particles += new Particle(
playerX,
playerY,
-facingX * 20.0,
-facingY * 20.0,
4.0,
185,
0.6
)
}
}
}
// --------------------------------------------------------------
// DRAW HELPERS
// --------------------------------------------------------------
def drawGlow(
g: Graphics2D,
x: Int,
y: Int,
radius: Int,
color: Color,
alpha: Float
): Unit = {
val old = g.getComposite
g.setComposite(
AlphaComposite.SrcOver.derive(alpha)
)
g.setColor(color)
g.fillOval(
x - radius,
y - radius,
radius * 2,
radius * 2
)
g.setComposite(old)
}
def drawCentered(
g: Graphics2D,
text: String,
x: Int,
y: Int
): Unit = {
val width = g.getFontMetrics.stringWidth(text)
g.drawString(
text,
x - width / 2,
y
)
}
// --------------------------------------------------------------
// COSMIC SKY
// --------------------------------------------------------------
def drawCosmicSky(
g: Graphics2D
): Unit = {
val moonX = screenX(1580)
val moonY = screenY(150)
drawGlow(
g,
moonX,
moonY,
125,
PURPLE,
0.13f
)
g.setColor(new Color(255, 230, 167))
g.fillOval(
moonX - 48,
moonY - 48,
96,
96
)
g.setColor(new Color(255, 248, 207))
g.fillOval(
moonX - 34,
moonY - 35,
66,
66
)
stars.foreach { s =>
val x = screenX(s.x)
val y = screenY(s.y)
if (
x >= 0 &&
y >= 0 &&
x < SCREEN_W &&
y < SCREEN_H
) {
val brightness =
130 + math.abs(
(math.sin(frames * 0.08 + s.phase) * 115).toInt
)
g.setColor(
new Color(255, 255, 255, brightness)
)
g.fillOval(
x,
y,
s.size,
s.size
)
}
}
}
// --------------------------------------------------------------
// FOREST GROUND
// --------------------------------------------------------------
def drawGround(
g: Graphics2D,
w: Int,
h: Int
): Unit = {
val old = g.getComposite
g.setPaint(
new GradientPaint(
0f,
0f,
new Color(24, 98, 73),
0f,
h.toFloat,
new Color(8, 43, 42)
)
)
g.fillRect(0, 0, w, h)
// Grass patches
for (i <- 0 until 18) {
val wx = (i * 131 + 55) % WORLD_W
val wy = (i * 181 + 170) % WORLD_H
val x = screenX(wx)
val y = screenY(wy)
g.setColor(
new Color(66, 181, 110, 35)
)
g.fillOval(
x - 65,
y - 35,
130,
70
)
}
// Soft cosmic grid
g.setComposite(
AlphaComposite.SrcOver.derive(0.08f)
)
g.setColor(CYAN)
var x = 0
while (x < w) {
g.drawLine(x, 0, x, h)
x += 80
}
var y = 0
while (y < h) {
g.drawLine(0, y, w, y)
y += 80
}
g.setComposite(old)
}
// --------------------------------------------------------------
// FOREST PATH
// --------------------------------------------------------------
def drawPaths(g: Graphics2D): Unit = {
val routes = Array(
(300.0, 1110.0, 300.0, 900.0),
(300.0, 900.0, 590.0, 700.0),
(590.0, 700.0, 980.0, 920.0),
(980.0, 920.0, 1330.0, 920.0),
(1330.0, 920.0, 1640.0, 760.0),
(1640.0, 760.0, 1530.0, 560.0),
(1530.0, 560.0, 1040.0, 345.0)
)
val oldStroke = g.getStroke
g.setStroke(
new BasicStroke(
100.0f,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
)
)
g.setColor(new Color(19, 49, 51))
routes.foreach { p =>
g.drawLine(
screenX(p._1),
screenY(p._2),
screenX(p._3),
screenY(p._4)
)
}
g.setStroke(
new BasicStroke(
75.0f,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
)
)
g.setColor(new Color(60, 112, 92, 180))
routes.foreach { p =>
g.drawLine(
screenX(p._1),
screenY(p._2),
screenX(p._3),
screenY(p._4)
)
}
g.setStroke(oldStroke)
}
// --------------------------------------------------------------
// TREES
// --------------------------------------------------------------
def drawTree(
g: Graphics2D,
tree: GardenTree
): Unit = {
val x = screenX(tree.x)
val y = screenY(tree.y)
val s = tree.scale
if (
x < -150 ||
y < -180 ||
x > SCREEN_W + 150 ||
y > SCREEN_H + 150
) {
return
}
drawGlow(
g,
x,
y - (85 * s).toInt,
(48 * s).toInt,
hueColor(tree.hue),
0.08f
)
// Shadow
g.setColor(new Color(0, 0, 0, 65))
g.fillOval(
(x - 44 * s).toInt,
(y - 13 * s).toInt,
(88 * s).toInt,
(25 * s).toInt
)
// Trunk
g.setColor(new Color(91, 54, 39))
g.fillRoundRect(
(x - 9 * s).toInt,
(y - 77 * s).toInt,
(18 * s).toInt,
(82 * s).toInt,
9,
9
)
// Tree leaves
g.setColor(
hueColor(tree.hue, 0.65f, 0.47f)
)
g.fillOval(
(x - 43 * s).toInt,
(y - 143 * s).toInt,
(82 * s).toInt,
(84 * s).toInt
)
g.setColor(
hueColor(tree.hue, 0.61f, 0.62f)
)
g.fillOval(
(x - 32 * s).toInt,
(y - 157 * s).toInt,
(67 * s).toInt,
(68 * s).toInt
)
g.setColor(new Color(170, 255, 207, 110))
g.drawArc(
(x - 27 * s).toInt,
(y - 149 * s).toInt,
(54 * s).toInt,
(48 * s).toInt,
40,
120
)
}
// --------------------------------------------------------------
// FLOWERS
// --------------------------------------------------------------
def drawFlower(
g: Graphics2D,
flower: GardenFlower
): Unit = {
val x = screenX(flower.x)
val y = screenY(flower.y)
if (
x < -30 ||
y < -30 ||
x > SCREEN_W + 30 ||
y > SCREEN_H + 30
) {
return
}
val pulse =
0.86 + 0.14 * math.sin(frames * 0.07 + flower.phase)
val radius =
math.max(3, (flower.size * pulse).toInt)
val color =
hueColor(flower.hue, 0.75f, 1.0f)
if (flower.special && glowMode) {
drawGlow(
g,
x,
y,
radius * 5,
color,
0.10f
)
}
g.setColor(new Color(32, 114, 66))
g.drawLine(
x,
y + 3,
x,
y + radius * 3
)
g.setColor(color)
val petalsCount =
if (flower.special) 8
else 5
var i = 0
while (i < petalsCount) {
val angle =
i * math.Pi * 2.0 / petalsCount
val px =
x + (math.cos(angle) * radius * 0.8).toInt
val py =
y + (math.sin(angle) * radius * 0.8).toInt
g.fillOval(
px - radius / 2,
py - radius / 2,
radius,
radius
)
i += 1
}
g.setColor(GOLD)
g.fillOval(
x - 3,
y - 3,
6,
6
)
}
// --------------------------------------------------------------
// MAGICAL WATERFALL
// --------------------------------------------------------------
def drawWaterfall(
g: Graphics2D
): Unit = {
val x = screenX(1500)
val y = screenY(170)
// Rocky cliff
g.setColor(new Color(71, 89, 91))
g.fillRoundRect(
x - 145,
y - 20,
290,
505,
35,
35
)
g.setColor(new Color(117, 137, 127))
g.fillOval(
x - 140,
y - 25,
280,
100
)
g.setColor(new Color(50, 77, 80))
g.fillOval(
x - 120,
y + 5,
75,
55
)
// Main water stream
g.setPaint(
new GradientPaint(
x - 55,
y.toFloat,
new Color(179, 249, 255),
x + 55,
(y + 425).toFloat,
new Color(20, 116, 222)
)
)
g.fillRoundRect(
x - 56,
y + 30,
112,
395,
24,
24
)
// Water stream highlights
var i = 0
while (i < 17) {
val sx = x - 44 + i * 5
val sy =
y + 40 + ((frames * 6 + i * 29) % 360).toInt
g.setColor(
new Color(238, 255, 255, 185)
)
g.fillRoundRect(
sx,
sy,
3,
30,
5,
5
)
i += 1
}
// Water pool
g.setColor(
new Color(31, 170, 224, 235)
)
g.fillOval(
x - 165,
y + 380,
330,
100
)
g.setColor(
new Color(158, 247, 255, 170)
)
g.fillOval(
x - 90,
y + 397,
180,
47
)
// Moving ripples
i = 0
while (i < 6) {
val offset =
((frames * 3 + i * 27) % 95).toInt
g.setColor(
new Color(225, 255, 255, 115)
)
g.drawOval(
x - 50 - offset / 2,
y + 405 - offset / 6,
100 + offset,
30 + offset / 3
)
i += 1
}
// Splash particles
for (j <- 0 until 10) {
val sx =
x - 95 + ((j * 23 + frames * 2) % 190).toInt
val sy =
y + 408 + (j * 13) % 35
g.setColor(
new Color(216, 250, 255, 160)
)
g.fillOval(
sx,
sy,
4,
7
)
}
}
// --------------------------------------------------------------
// COSMIC CRYSTALS
// --------------------------------------------------------------
def drawCrystal(
g: Graphics2D,
crystal: Crystal
): Unit = {
if (crystal.collected) {
return
}
val x = screenX(crystal.x)
val y = screenY(crystal.y)
if (
x < -120 ||
y < -120 ||
x > SCREEN_W + 120 ||
y > SCREEN_H + 120
) {
return
}
val color =
hueColor(crystal.hue, 0.8f, 1.0f)
val pulse =
1.0 + 0.12 * math.sin(frames * 0.09 + crystal.hue)
val r = (18 * pulse).toInt
if (glowMode) {
drawGlow(
g,
x,
y,
r * 5,
color,
0.12f
)
}
val pointsX = Array(
x,
x + r,
x,
x - r
)
val pointsY = Array(
y - r,
y,
y + r,
y
)
g.setColor(color)
g.fillPolygon(
pointsX,
pointsY,
4
)
g.setColor(WHITE)
g.setStroke(new BasicStroke(2.0f))
g.drawPolygon(
pointsX,
pointsY,
4
)
g.setColor(
new Color(255, 255, 255, 180)
)
g.drawLine(
x,
y - r + 4,
x + r / 2,
y
)
// Crystal label
g.setFont(
new Font("SansSerif", Font.BOLD, 10)
)
g.setColor(WHITE)
drawCentered(
g,
crystal.label,
x,
y - r - 20
)
g.setColor(CYAN)
drawCentered(
g,
"E - COLLECT",
x,
y + r + 20
)
}
// --------------------------------------------------------------
// PORTAL
// --------------------------------------------------------------
def drawPortal(
g: Graphics2D
): Unit = {
val x = screenX(portalX)
val y = screenY(portalY)
val ready =
collectedCount >= GOAL_COUNT
val pulse =
1.0 + math.sin(frames * 0.05) * 0.06
val w = (125 * pulse).toInt
val h = (155 * pulse).toInt
val portalColor =
if (ready) MINT
else PURPLE
if (glowMode) {
drawGlow(
g,
x,
y,
115,
portalColor,
0.12f
)
}
g.setColor(new Color(7, 14, 35))
g.fillOval(
x - w / 2,
y - h / 2,
w,
h
)
g.setColor(portalColor)
g.setStroke(
new BasicStroke(6.0f)
)
g.drawOval(
x - w / 2,
y - h / 2,
w,
h
)
g.setColor(
new Color(91, 235, 250, 145)
)
g.setStroke(
new BasicStroke(3.0f)
)
g.drawOval(
x - w / 2 + 12,
y - h / 2 + 13,
w - 24,
h - 26
)
// Rotating magical particles
for (i <- 0 until 8) {
val angle =
frames * 0.035 + i * math.Pi * 2.0 / 8.0
val px =
x + (math.cos(angle) * 48).toInt
val py =
y + (math.sin(angle) * 65).toInt
g.setColor(
if (i % 2 == 0) CYAN
else PINK
)
g.fillOval(
px - 4,
py - 4,
8,
8
)
}
g.setFont(
new Font("SansSerif", Font.BOLD, 12)
)
g.setColor(
if (ready) MINT
else WHITE
)
drawCentered(
g,
if (ready) "PORTAL UNLOCKED"
else "LOCKED PORTAL",
x,
y + h / 2 + 25
)
}
// --------------------------------------------------------------
// PLAYER CHARACTER
// --------------------------------------------------------------
def drawPlayer(
g: Graphics2D
): Unit = {
val x = screenX(playerX)
val y = screenY(playerY)
val bob =
math.sin(frames * 0.2) * 2.0
if (glowMode) {
drawGlow(
g,
x,
y,
42,
CYAN,
0.09f
)
}
// Shadow
g.setColor(
new Color(0, 0, 0, 100)
)
g.fillOval(
x - 21,
y + 17,
42,
12
)
// Legs
g.setColor(
new Color(24, 39, 66)
)
g.fillRoundRect(
x - 13,
y + bob.toInt,
11,
21,
5,
5
)
g.fillRoundRect(
x + 3,
y - bob.toInt,
11,
21,
5,
5
)
// Boots
g.setColor(CYAN)
g.fillRoundRect(
x - 14,
y + 16,
13,
6,
4,
4
)
g.fillRoundRect(
x + 2,
y + 16,
13,
6,
4,
4
)
// Body
g.setColor(
new Color(35, 65, 119)
)
g.fillRoundRect(
x - 18,
y - 26 + bob.toInt,
36,
34,
12,
12
)
g.setColor(
new Color(49, 189, 193)
)
g.fillRoundRect(
x - 14,
y - 22 + bob.toInt,
28,
26,
10,
10
)
// Chest light
g.setColor(WHITE)
g.fillOval(
x - 5,
y - 17 + bob.toInt,
10,
10
)
// Head
g.setColor(
new Color(239, 205, 175)
)
g.fillOval(
x - 12,
y - 48 + bob.toInt,
24,
25
)
// Hair
g.setColor(
new Color(36, 28, 46)
)
g.fillArc(
x - 13,
y - 54 + bob.toInt,
26,
27,
0,
180
)
// Eyes / visor
g.setColor(
new Color(15, 43, 69)
)
g.fillRoundRect(
x - 9,
y - 39 + bob.toInt,
18,
8,
5,
5
)
g.setColor(CYAN)
g.drawLine(
x - 5,
y - 35 + bob.toInt,
x + 5,
y - 35 + bob.toInt
)
// Dash trail
if (dashTime > 0.0) {
g.setColor(
new Color(100, 249, 240, 150)
)
g.setStroke(
new BasicStroke(4.0f)
)
g.drawLine(
x - (facingX * 45).toInt,
y + 5 - (facingY * 45).toInt,
x,
y + 5
)
}
}
// --------------------------------------------------------------
// PARTICLES
// --------------------------------------------------------------
def drawParticles(
g: Graphics2D
): Unit = {
particles.foreach { p =>
val x = screenX(p.x)
val y = screenY(p.y)
if (
x >= -10 &&
y >= -10 &&
x <= SCREEN_W + 10 &&
y <= SCREEN_H + 10
) {
val alpha =
clamp(p.life / 3.0, 0.0, 0.85).toFloat
val old = g.getComposite
g.setComposite(
AlphaComposite.SrcOver.derive(alpha)
)
g.setColor(
hueColor(p.hue, 0.65f, 1.0f)
)
val sz =
math.max(2, p.size.toInt)
g.fillOval(
x,
y,
sz,
sz
)
g.setComposite(old)
}
}
}
// --------------------------------------------------------------
// MINI MAP
// --------------------------------------------------------------
def drawMiniMap(
g: Graphics2D,
w: Int
): Unit = {
val x = w - 190
val y = 116
val mw = 168
val mh = 118
g.setColor(
new Color(5, 14, 29, 220)
)
g.fillRoundRect(
x, y, mw, mh, 14, 14
)
g.setColor(
new Color(75, 231, 222, 180)
)
g.drawRoundRect(
x, y, mw, mh, 14, 14
)
def mapX(v: Double): Int = {
x + 8 + (v / WORLD_W * (mw - 16)).toInt
}
def mapY(v: Double): Int = {
y + 8 + (v / WORLD_H * (mh - 16)).toInt
}
// Remaining crystals
crystals.filter(!_.collected).foreach { c =>
g.setColor(
hueColor(c.hue)
)
g.fillOval(
mapX(c.x) - 3,
mapY(c.y) - 3,
6,
6
)
}
// Portal
g.setColor(MINT)
g.fillOval(
mapX(portalX) - 4,
mapY(portalY) - 4,
8,
8
)
// Player position
g.setColor(WHITE)
g.fillOval(
mapX(playerX) - 4,
mapY(playerY) - 4,
8,
8
)
g.setFont(
new Font("SansSerif", Font.BOLD, 9)
)
g.setColor(WHITE)
g.drawString(
"MAGIC MAP",
x + 10,
y + 13
)
}
// --------------------------------------------------------------
// HUD
// --------------------------------------------------------------
def drawHUD(
g: Graphics2D,
w: Int,
h: Int
): Unit = {
// Top left
g.setColor(
new Color(5, 17, 33, 225)
)
g.fillRoundRect(
16, 15, 440, 90, 18, 18
)
g.setColor(
new Color(75, 235, 219, 180)
)
g.drawRoundRect(
16, 15, 440, 90, 18, 18
)
g.setFont(
new Font("SansSerif", Font.BOLD, 20)
)
g.setColor(CYAN)
g.drawString(
"BIXUU",
31,
42
)
g.setFont(
new Font("SansSerif", Font.BOLD, 12)
)
g.setColor(WHITE)
g.drawString(
"SPRING SEASON EVENT 02",
31,
61
)
g.drawString(
"CRYSTALS " + collectedCount + " / " + GOAL_COUNT,
31,
88
)
// Progress bar
g.setColor(
new Color(36, 54, 72)
)
g.fillRoundRect(
228, 76, 205, 9, 8, 8
)
g.setColor(
if (collectedCount == GOAL_COUNT) MINT
else PINK
)
g.fillRoundRect(
228,
76,
(205.0 * collectedCount / GOAL_COUNT).toInt,
9,
8,
8
)
// Time
val minutes =
remainingSeconds.toInt / 60
val seconds =
remainingSeconds.toInt % 60
val timeText =
"%02d:%02d".format(minutes, seconds)
g.setColor(
new Color(5, 17, 33, 225)
)
g.fillRoundRect(
w - 185, 18, 165, 69, 15, 15
)
g.setColor(GOLD)
g.setFont(
new Font("Monospaced", Font.BOLD, 23)
)
g.drawString(
timeText,
w - 163,
51
)
g.setFont(
new Font("SansSerif", Font.PLAIN, 10)
)
g.setColor(WHITE)
g.drawString(
"TIME REMAINING",
w - 161,
72
)
// Bottom controls
val by = h - 50
g.setColor(
new Color(5, 17, 32, 225)
)
g.fillRoundRect(
14, by, w - 28, 36, 12, 12
)
g.setFont(
new Font("SansSerif", Font.BOLD, 11)
)
g.setColor(MINT)
g.drawString(
"WASD / ARROWS : MOVE",
27,
by + 22
)
g.setColor(CYAN)
g.drawString(
"E : COLLECT",
222,
by + 22
)
g.setColor(PINK)
g.drawString(
"SPACE : DASH",
335,
by + 22
)
g.setColor(GOLD)
g.drawString(
"F : GLOW",
468,
by + 22
)
g.setColor(WHITE)
g.drawString(
"M : MAP ESC : PAUSE R : RESTART",
585,
by + 22
)
if (showMap) {
drawMiniMap(g, w)
}
// Toast message
if (toastTime > 0.0) {
val tw = math.min(
w - 50,
math.max(
350,
g.getFontMetrics.stringWidth(toast) + 38
)
)
val tx = (w - tw) / 2
g.setColor(
new Color(5, 16, 33, 235)
)
g.fillRoundRect(
tx, h - 100, tw, 34, 14, 14
)
g.setColor(CYAN)
g.drawRoundRect(
tx, h - 100, tw, 34, 14, 14
)
g.setFont(
new Font("SansSerif", Font.BOLD, 11)
)
g.setColor(WHITE)
drawCentered(
g,
toast,
w / 2,
h - 79
)
}
// Ambient glitch effect
if (
glitchActive &&
frames % 4 < 2 &&
started &&
!paused &&
!won &&
!lost
) {
val old = g.getComposite
g.setComposite(
AlphaComposite.SrcOver.derive(0.12f)
)
g.setColor(PINK)
g.fillRect(0, 120, w, 3)
g.setColor(CYAN)
g.fillRect(0, 126, w, 2)
g.setComposite(old)
}
}
// --------------------------------------------------------------
// INTRO SCREEN
// --------------------------------------------------------------
def drawIntro(
g: Graphics2D,
w: Int,
h: Int
): Unit = {
g.setColor(
new Color(3, 8, 24, 205)
)
g.fillRect(
0, 0, w, h
)
val cx = w / 2
val cy = h / 2
drawGlow(
g,
cx,
cy - 70,
190,
PURPLE,
0.12f
)
g.setFont(
new Font("SansSerif", Font.BOLD, 20)
)
g.setColor(MINT)
drawCentered(
g,
"BIXUU",
cx,
cy - 178
)
g.setFont(
new Font("SansSerif", Font.BOLD, 21)
)
g.setColor(WHITE)
drawCentered(
g,
"SPRING SEASON EVENT 02",
cx,
cy - 139
)
g.setFont(
new Font("SansSerif", Font.BOLD, 43)
)
g.setColor(CYAN)
drawCentered(
g,
"AURORA FALLS",
cx,
cy - 83
)
g.setFont(
new Font("SansSerif", Font.BOLD, 23)
)
g.setColor(PINK)
drawCentered(
g,
"THE LOST SPRING",
cx,
cy - 45
)
g.setFont(
new Font("SansSerif", Font.PLAIN, 15)
)
g.setColor(WHITE)
drawCentered(
g,
"Explore a magical forest, collect six crystals",
cx,
cy + 1
)
drawCentered(
g,
"and unlock the secret Cosmic Waterfall Portal.",
cx,
cy + 24
)
val bx = cx - 145
val by = cy + 57
g.setColor(
new Color(20, 102, 91)
)
g.fillRoundRect(
bx, by, 290, 58, 18, 18
)
g.setStroke(
new BasicStroke(2.5f)
)
g.setColor(CYAN)
g.drawRoundRect(
bx, by, 290, 58, 18, 18
)
g.setFont(
new Font("SansSerif", Font.BOLD, 20)
)
g.setColor(WHITE)
drawCentered(
g,
"START ADVENTURE",
cx,
by + 36
)
g.setFont(
new Font("SansSerif", Font.PLAIN, 13)
)
g.setColor(GOLD)
drawCentered(
g,
"PRESS ENTER OR CLICK TO PLAY",
cx,
by + 91
)
g.setFont(
new Font("SansSerif", Font.PLAIN, 11)
)
g.setColor(
new Color(205, 226, 238)
)
drawCentered(
g,
"ORIGINAL OFFLINE SCALA GAME",
cx,
h - 22
)
}
// --------------------------------------------------------------
// PAUSE / WIN / GAME OVER SCREEN
// --------------------------------------------------------------
def drawEndScreen(
g: Graphics2D,
w: Int,
h: Int
): Unit = {
g.setColor(
new Color(3, 8, 22, 215)
)
g.fillRect(
0, 0, w, h
)
val message =
if (won) "SPRING RESTORED!"
else if (lost) "TIME HAS RUN OUT"
else "GAME PAUSED"
val subtitle =
if (won) "You unlocked the Cosmic Waterfall Portal!"
else if (lost) "Try again and restore all six crystals."
else "Your adventure is waiting."
g.setFont(
new Font("SansSerif", Font.BOLD, 39)
)
g.setColor(
if (won) MINT
else if (lost) PINK
else CYAN
)
drawCentered(
g,
message,
w / 2,
h / 2 - 50
)
g.setFont(
new Font("SansSerif", Font.PLAIN, 16)
)
g.setColor(WHITE)
drawCentered(
g,
subtitle,
w / 2,
h / 2 - 10
)
g.setFont(
new Font("SansSerif", Font.BOLD, 17)
)
g.setColor(GOLD)
drawCentered(
g,
"PRESS R TO RESTART",
w / 2,
h / 2 + 42
)
}
// --------------------------------------------------------------
// GAME PANEL
// --------------------------------------------------------------
final class GamePanel extends JPanel {
setPreferredSize(
new Dimension(SCREEN_W, SCREEN_H)
)
setBackground(BG_TOP)
setFocusable(true)
setDoubleBuffered(true)
setCursor(
new Cursor(Cursor.CROSSHAIR_CURSOR)
)
// Keyboard
addKeyListener(
new KeyAdapter {
override def keyPressed(e: KeyEvent): Unit = {
val k = e.getKeyCode
val fresh = !keys.contains(k)
keys += k
if (fresh) {
k match {
case KeyEvent.VK_ENTER =>
if (!started) startAdventure()
case KeyEvent.VK_E =>
interact()
case KeyEvent.VK_SPACE =>
if (!started) startAdventure()
else triggerDash()
case KeyEvent.VK_F =>
glowMode = !glowMode
case KeyEvent.VK_M =>
showMap = !showMap
case KeyEvent.VK_ESCAPE =>
if (started && !won && !lost) {
paused = !paused
}
case KeyEvent.VK_R =>
resetGame()
case _ =>
}
}
}
override def keyReleased(e: KeyEvent): Unit = {
keys -= e.getKeyCode
}
}
)
// Mouse
addMouseListener(
new MouseAdapter {
override def mouseClicked(e: MouseEvent): Unit = {
requestFocusInWindow()
if (!started) {
startAdventure()
} else if (paused || won || lost) {
resetGame()
} else {
interact()
}
}
}
)
// Main rendering
override protected def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(graphics)
val g =
graphics.create().asInstanceOf[Graphics2D]
try {
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
)
g.setRenderingHint(
RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY
)
val w = getWidth
val h = getHeight
// Camera follows player
cameraX = clamp(
playerX - w / 2.0,
0.0,
WORLD_W - w
)
cameraY = clamp(
playerY - h / 2.0,
0.0,
WORLD_H - h
)
// Render layers
drawGround(g, w, h)
drawCosmicSky(g)
drawPaths(g)
drawWaterfall(g)
trees.foreach(t => drawTree(g, t))
flowers.foreach(f => drawFlower(g, f))
crystals.foreach(c => drawCrystal(g, c))
drawPortal(g)
drawParticles(g)
drawPlayer(g)
if (started) {
drawHUD(g, w, h)
}
if (!started) {
drawIntro(g, w, h)
} else if (paused || won || lost) {
drawEndScreen(g, w, h)
}
} finally {
g.dispose()
}
}
}
// --------------------------------------------------------------
// START GAME WINDOW
// --------------------------------------------------------------
def main(args: Array[String]): Unit = {
setupWorld()
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
window = new JFrame(APP_TITLE)
panel = new GamePanel()
window.setContentPane(panel)
window.setDefaultCloseOperation(
WindowConstants.EXIT_ON_CLOSE
)
window.setMinimumSize(
new Dimension(900, 620)
)
window.setSize(
SCREEN_W,
SCREEN_H
)
window.setLocationRelativeTo(null)
window.setVisible(true)
panel.requestFocusInWindow()
// Approximately 60 FPS
gameTimer = new Timer(
16,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
updateGame(0.016)
if (panel != null) {
panel.repaint()
}
}
}
)
gameTimer.start()
}
}
)
}
}
// Launch
BixuuAuroraFalls.main(Array[String]())