Code Sketch
yoii code
Category: Programming
import java.awt.BasicStroke
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Cursor
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Font
import java.awt.GradientPaint
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Point
import java.awt.Toolkit
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.nio.file.Files
import java.awt.RenderingHints
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.MouseMotionAdapter
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.datatransfer.DataFlavor
import java.awt.geom.Path2D
import java.util.Random
import javax.swing.DefaultListModel
import javax.swing.JButton
import javax.swing.JColorChooser
import javax.swing.ImageIcon
import javax.swing.filechooser.FileNameExtensionFilter
import javax.swing.JFileChooser
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.JOptionPane
import javax.swing.JPanel
import javax.swing.JScrollPane
import javax.swing.JSplitPane
import javax.swing.JTabbedPane
import javax.swing.JTextArea
import javax.swing.JTextField
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.SwingUtilities
import javax.swing.Timer
import javax.swing.WindowConstants
import javax.swing.TransferHandler
import scala.collection.mutable.ArrayBuffer
// ============================================================
// ULTRA LEGEND X9
// DRAW + OBJECT + SHAPE + ANIMATION + GAMES STUDIO
// FULL STABLE VERSION
// ============================================================
// ============================================================
// CONSTANTS
// ============================================================
val CANVAS_W: Int = 1000
val CANVAS_H: Int = 650
val CENTER_X: Int = CANVAS_W / 2
val CENTER_Y: Int = CANVAS_H / 2
val RANDOM_GEN =
new Random()
// ============================================================
// DATA MODELS
// ============================================================
case class DrawItem(
points: Vector[Point],
stroke: Color,
fill: Color,
filled: Boolean,
name: String,
visible: Boolean,
locked: Boolean
)
case class PaddleLine(
p1: Point,
p2: Point
)
case class GameInfo(
name: String,
mode: String,
description: String
)
// ============================================================
// GLOBAL DRAW STATE
// ============================================================
var drawItems =
ArrayBuffer[DrawItem]()
var undoStack =
ArrayBuffer[ArrayBuffer[DrawItem]]()
var redoStack =
ArrayBuffer[ArrayBuffer[DrawItem]]()
var activeTool: String =
"PENCIL"
var strokeColor: Color =
new Color(25, 90, 220)
var fillColor: Color =
new Color(255, 180, 40)
var brushWidth: Float =
5.0f
var showGrid: Boolean =
true
var snapToGrid: Boolean =
false
var selectedIndex: Int =
-1
var drawingNow: Boolean =
false
var movingNow: Boolean =
false
var resizingNow: Boolean =
false
var resizeHandle: String =
""
var animationFrames =
ArrayBuffer[ArrayBuffer[DrawItem]]()
var animationClipboard =
ArrayBuffer[DrawItem]()
var animationCurrentFrame: Int =
0
var animationFps: Int =
8
var animationStudioOpen: Boolean =
false
var lastMousePoint: Point =
new Point(0, 0)
var originalPoints: Vector[Point] =
Vector.empty
var tempPoints =
ArrayBuffer[Point]()
var generatedCode: String =
""
var generatedAnimationCode: String =
""
// ============================================================
// IMPORTED MEDIA STATE
// ============================================================
var importedImage: BufferedImage =
null
var importedImageOriginal: BufferedImage =
null
var importedImagePath: String =
""
var importedImageName: String =
""
var importedVideoPath: String =
""
var importedVideoName: String =
""
var mainFrame: JFrame =
null
var canvas: JPanel =
null
var codeArea: JTextArea =
null
var statusLabel: JLabel =
null
var objectSearch: JTextField =
null
var objectList: JList[String] =
null
var shapeList: JList[String] =
null
// ============================================================
// UI HELPERS
// ============================================================
def button(text: String): JButton = {
val b =
new JButton(text)
b.setFocusable(false)
b.setFont(
new Font(
"Arial",
Font.BOLD,
11
)
)
b
}
def setStatus(text: String): Unit = {
if (statusLabel != null) {
statusLabel.setText(text)
}
}
def repaintCanvas(): Unit = {
if (canvas != null) {
canvas.repaint()
}
}
// ============================================================
// HISTORY
// ============================================================
def cloneItems(
source: ArrayBuffer[DrawItem]
): ArrayBuffer[DrawItem] = {
val output =
ArrayBuffer[DrawItem]()
var i: Int = 0
while (i < source.length) {
val item =
source(i)
val pts =
item.points.map { p =>
new Point(
p.x,
p.y
)
}
output +=
item.copy(
points = pts
)
i += 1
}
output
}
def saveHistory(): Unit = {
undoStack +=
cloneItems(drawItems)
redoStack.clear()
while (undoStack.length > 60) {
undoStack.remove(0)
}
}
def undo(): Unit = {
if (undoStack.nonEmpty) {
redoStack +=
cloneItems(drawItems)
val previous =
undoStack.last
undoStack.remove(
undoStack.length - 1
)
drawItems.clear()
drawItems ++=
cloneItems(previous)
selectedIndex =
-1
repaintCanvas()
setStatus(
"UNDO COMPLETE"
)
} else {
setStatus(
"NOTHING TO UNDO"
)
}
}
def redo(): Unit = {
if (redoStack.nonEmpty) {
undoStack +=
cloneItems(drawItems)
val nextState =
redoStack.last
redoStack.remove(
redoStack.length - 1
)
drawItems.clear()
drawItems ++=
cloneItems(nextState)
selectedIndex =
-1
repaintCanvas()
setStatus(
"REDO COMPLETE"
)
} else {
setStatus(
"NOTHING TO REDO"
)
}
}
// ============================================================
// POINT HELPERS
// ============================================================
def distance(
a: Point,
b: Point
): Double = {
math.hypot(
b.x - a.x,
b.y - a.y
)
}
def snapped(value: Int): Int = {
if (snapToGrid) {
(value / 10) * 10
} else {
value
}
}
def canvasPoint(
e: MouseEvent
): Point = {
val x =
math.max(
0,
math.min(
CANVAS_W - 1,
snapped(e.getX)
)
)
val y =
math.max(
0,
math.min(
CANVAS_H - 1,
snapped(e.getY)
)
)
new Point(
x,
y
)
}
def closed(
points: Iterable[Point]
): Boolean = {
val seq =
points.toVector
seq.length >= 3 &&
distance(
seq.head,
seq.last
) <= 45.0
}
// ============================================================
// BOUNDS
// ============================================================
def boundsOf(
item: DrawItem
): java.awt.Rectangle = {
if (item.points.isEmpty) {
new java.awt.Rectangle(
0,
0,
1,
1
)
} else {
var minX =
item.points.head.x
var maxX =
item.points.head.x
var minY =
item.points.head.y
var maxY =
item.points.head.y
var i: Int = 1
while (i < item.points.length) {
val p =
item.points(i)
if (p.x < minX) {
minX = p.x
}
if (p.x > maxX) {
maxX = p.x
}
if (p.y < minY) {
minY = p.y
}
if (p.y > maxY) {
maxY = p.y
}
i += 1
}
new java.awt.Rectangle(
minX,
minY,
math.max(
1,
maxX - minX
),
math.max(
1,
maxY - minY
)
)
}
}
// ============================================================
// SEGMENT HIT TEST
// ============================================================
def segmentDistance(
p: Point,
a: Point,
b: Point
): Double = {
val dx: Double =
b.x - a.x
val dy: Double =
b.y - a.y
if (
dx == 0.0 &&
dy == 0.0
) {
distance(
p,
a
)
} else {
val dot: Double =
(p.x - a.x) * dx +
(p.y - a.y) * dy
val len2: Double =
dx * dx +
dy * dy
val t: Double =
math.max(
0.0,
math.min(
1.0,
dot / len2
)
)
val cx: Double =
a.x + t * dx
val cy: Double =
a.y + t * dy
math.hypot(
p.x - cx,
p.y - cy
)
}
}
// ============================================================
// POLYGON HIT TEST
// ============================================================
def insidePolygon(
p: Point,
points: Seq[Point]
): Boolean = {
if (points.length < 3) {
false
} else {
var inside: Boolean =
false
var j: Int =
points.length - 1
var i: Int =
0
while (i < points.length) {
val xi =
points(i).x
val yi =
points(i).y
val xj =
points(j).x
val yj =
points(j).y
val hit =
((yi > p.y) != (yj > p.y)) &&
(
p.x <
(
xj - xi
).toDouble *
(
p.y - yi
).toDouble /
(
yj - yi +
0.000001
) +
xi
)
if (hit) {
inside =
!inside
}
j =
i
i += 1
}
inside
}
}
// ============================================================
// OBJECT HIT TEST
// ============================================================
def findObject(
p: Point
): Int = {
var result: Int =
-1
var i: Int =
drawItems.length - 1
while (
i >= 0 &&
result < 0
) {
val item =
drawItems(i)
if (item.visible) {
val r =
boundsOf(item)
val expanded =
new java.awt.Rectangle(
r.x - 14,
r.y - 14,
r.width + 28,
r.height + 28
)
if (expanded.contains(p)) {
if (
item.filled &&
closed(item.points) &&
insidePolygon(
p,
item.points.toSeq
)
) {
result =
i
} else {
var j: Int =
1
while (
j < item.points.length &&
result < 0
) {
if (
segmentDistance(
p,
item.points(j - 1),
item.points(j)
) < 16.0
) {
result =
i
}
j += 1
}
}
}
}
i -= 1
}
result
}
// ============================================================
// RESIZE HANDLES
// ============================================================
def handles(
r: java.awt.Rectangle
): Array[(String, Point)] = {
val mx =
r.x + r.width / 2
val my =
r.y + r.height / 2
Array(
("TL", new Point(r.x, r.y)),
("TM", new Point(mx, r.y)),
("TR", new Point(r.x + r.width, r.y)),
("ML", new Point(r.x, my)),
("MR", new Point(r.x + r.width, my)),
("BL", new Point(r.x, r.y + r.height)),
("BM", new Point(mx, r.y + r.height)),
("BR", new Point(r.x + r.width, r.y + r.height))
)
}
def resizeHandle(
item: DrawItem,
p: Point
): String = {
val r =
boundsOf(item)
val expanded =
new java.awt.Rectangle(
r.x - 8,
r.y - 8,
r.width + 16,
r.height + 16
)
val hs =
handles(expanded)
var result =
""
var i: Int =
0
while (
i < hs.length &&
result == ""
) {
if (
distance(
p,
hs(i)._2
) <= 13.0
) {
result =
hs(i)._1
}
i += 1
}
result
}
// ============================================================
// MOVE
// ============================================================
def moveSelected(
dx: Int,
dy: Int
): Unit = {
if (
selectedIndex >= 0 &&
selectedIndex < drawItems.length
) {
val item =
drawItems(selectedIndex)
if (!item.locked) {
val moved =
item.points.map { p =>
new Point(
p.x + dx,
p.y + dy
)
}
drawItems(selectedIndex) =
item.copy(
points = moved
)
}
}
}
// ============================================================
// RESIZE
// ============================================================
def resizeSelected(
p: Point
): Unit = {
if (
selectedIndex >= 0 &&
selectedIndex < drawItems.length &&
originalPoints.nonEmpty
) {
val item =
drawItems(selectedIndex)
var minX =
originalPoints.head.x
var maxX =
originalPoints.head.x
var minY =
originalPoints.head.y
var maxY =
originalPoints.head.y
var i: Int =
1
while (
i < originalPoints.length
) {
val q =
originalPoints(i)
if (q.x < minX) {
minX =
q.x
}
if (q.x > maxX) {
maxX =
q.x
}
if (q.y < minY) {
minY =
q.y
}
if (q.y > maxY) {
maxY =
q.y
}
i += 1
}
val oldW =
math.max(
1,
maxX - minX
)
val oldH =
math.max(
1,
maxY - minY
)
var newMinX =
minX
var newMaxX =
maxX
var newMinY =
minY
var newMaxY =
maxY
if (resizeHandle == "TL") {
newMinX = p.x
newMinY = p.y
} else if (resizeHandle == "TM") {
newMinY = p.y
} else if (resizeHandle == "TR") {
newMaxX = p.x
newMinY = p.y
} else if (resizeHandle == "ML") {
newMinX = p.x
} else if (resizeHandle == "MR") {
newMaxX = p.x
} else if (resizeHandle == "BL") {
newMinX = p.x
newMaxY = p.y
} else if (resizeHandle == "BM") {
newMaxY = p.y
} else if (resizeHandle == "BR") {
newMaxX = p.x
newMaxY = p.y
}
val newW =
math.max(
5,
newMaxX - newMinX
)
val newH =
math.max(
5,
newMaxY - newMinY
)
val scaled =
originalPoints.map { q =>
val nx =
newMinX +
(
(
q.x - minX
).toDouble *
newW /
oldW
).toInt
val ny =
newMinY +
(
(
q.y - minY
).toDouble *
newH /
oldH
).toInt
new Point(
nx,
ny
)
}
drawItems(selectedIndex) =
item.copy(
points = scaled
)
}
}
// ============================================================
// GEOMETRY
// ============================================================
def polygon(
sides: Int,
radius: Int,
start: Double
): Vector[Point] = {
val out =
ArrayBuffer[Point]()
var i: Int =
0
while (i <= sides) {
val angle =
start +
i.toDouble *
2.0 *
math.Pi /
sides.toDouble
out +=
new Point(
CENTER_X +
(
math.cos(angle) *
radius
).toInt,
CENTER_Y +
(
math.sin(angle) *
radius
).toInt
)
i += 1
}
out.toVector
}
def ellipse(
rx: Int,
ry: Int
): Vector[Point] = {
val out =
ArrayBuffer[Point]()
var deg: Int =
0
while (deg <= 360) {
val a =
deg *
math.Pi /
180.0
out +=
new Point(
CENTER_X +
(
math.cos(a) *
rx
).toInt,
CENTER_Y +
(
math.sin(a) *
ry
).toInt
)
deg += 3
}
out.toVector
}
def star(
count: Int,
radius: Int
): Vector[Point] = {
val out =
ArrayBuffer[Point]()
var i: Int =
0
while (
i <= count * 2
) {
val angle =
-math.Pi / 2.0 +
i.toDouble *
math.Pi /
count.toDouble
val rr =
if (i % 2 == 0) {
radius
} else {
(radius * 0.45).toInt
}
out +=
new Point(
CENTER_X +
(
math.cos(angle) *
rr
).toInt,
CENTER_Y +
(
math.sin(angle) *
rr
).toInt
)
i += 1
}
out.toVector
}
def heart(): Vector[Point] = {
val out =
ArrayBuffer[Point]()
var i: Int =
0
while (i <= 180) {
val t =
i *
math.Pi /
180.0
val x =
16.0 *
math.pow(
math.sin(t),
3
)
val y =
13.0 *
math.cos(t) -
5.0 *
math.cos(2.0 * t) -
2.0 *
math.cos(3.0 * t) -
math.cos(4.0 * t)
out +=
new Point(
CENTER_X +
(x * 13.0).toInt,
CENTER_Y -
(y * 13.0).toInt
)
i += 1
}
out.toVector
}
// ============================================================
// SHAPE LIBRARY
// ============================================================
val shapes =
Array(
"LINE",
"SQUARE",
"RECTANGLE",
"CIRCLE",
"ELLIPSE",
"TRIANGLE",
"DIAMOND",
"PENTAGON",
"HEXAGON",
"OCTAGON",
"STAR",
"STAR 6",
"HEART",
"ARROW",
"PLUS",
"CLOUD",
"SUN",
"MOON",
"RING",
"WAVE",
"SPIRAL",
"BURST",
"SNOWFLAKE"
)
var shapeIndex: Int =
0
val shapeModel =
new DefaultListModel[String]()
while (
shapeIndex < shapes.length
) {
shapeModel.addElement(
shapes(shapeIndex)
)
shapeIndex += 1
}
// ============================================================
// SHAPE GENERATOR
// ============================================================
def shapeGeometry(
name: String
): Vector[Point] = {
val n =
name.toUpperCase
if (n == "LINE") {
Vector(
new Point(100,120),
new Point(900,530)
)
} else if (n == "SQUARE") {
Vector(
new Point(300,150),
new Point(700,150),
new Point(700,500),
new Point(300,500),
new Point(300,150)
)
} else if (n == "RECTANGLE") {
Vector(
new Point(170,210),
new Point(830,210),
new Point(830,440),
new Point(170,440),
new Point(170,210)
)
} else if (n == "CIRCLE") {
ellipse(
210,
210
)
} else if (n == "ELLIPSE") {
ellipse(
290,
160
)
} else if (n == "TRIANGLE") {
polygon(
3,
250,
-math.Pi / 2.0
)
} else if (n == "DIAMOND") {
Vector(
new Point(CENTER_X,70),
new Point(860,CENTER_Y),
new Point(CENTER_X,580),
new Point(140,CENTER_Y),
new Point(CENTER_X,70)
)
} else if (n == "PENTAGON") {
polygon(
5,
240,
-math.Pi / 2.0
)
} else if (n == "HEXAGON") {
polygon(
6,
235,
0.0
)
} else if (n == "OCTAGON") {
polygon(
8,
235,
math.Pi / 8.0
)
} else if (n == "STAR") {
star(
5,
250
)
} else if (n == "STAR 6") {
star(
6,
250
)
} else if (n == "HEART") {
heart()
} else if (n == "ARROW") {
Vector(
new Point(130,245),
new Point(620,245),
new Point(620,135),
new Point(870,325),
new Point(620,515),
new Point(620,405),
new Point(130,405),
new Point(130,245)
)
} else if (n == "PLUS") {
Vector(
new Point(430,90),
new Point(570,90),
new Point(570,240),
new Point(720,240),
new Point(720,410),
new Point(570,410),
new Point(570,560),
new Point(430,560),
new Point(430,410),
new Point(280,410),
new Point(280,240),
new Point(430,240),
new Point(430,90)
)
} else if (n == "CLOUD") {
Vector(
new Point(160,470),
new Point(200,360),
new Point(330,270),
new Point(450,300),
new Point(535,195),
new Point(665,220),
new Point(790,320),
new Point(840,390),
new Point(810,470),
new Point(160,470)
)
} else if (n == "SUN") {
star(
16,
245
)
} else if (n == "MOON") {
ellipse(
225,
225
)
} else if (n == "RING") {
ellipse(
225,
225
)
} else if (n == "WAVE") {
val out =
ArrayBuffer[Point]()
var x: Int =
30
while (x <= 970) {
out +=
new Point(
x,
CENTER_Y +
(
math.sin(
(x - 30) * 0.045
) * 100.0
).toInt
)
x += 5
}
out.toVector
} else if (n == "SPIRAL") {
val out =
ArrayBuffer[Point]()
var angle: Double =
0.0
var radius: Double =
4.0
while (
angle < math.Pi * 8.0
) {
out +=
new Point(
CENTER_X +
(
math.cos(angle) *
radius
).toInt,
CENTER_Y +
(
math.sin(angle) *
radius
).toInt
)
angle += 0.08
radius += 1.5
}
out.toVector
} else if (n == "BURST") {
star(
20,
250
)
} else if (n == "SNOWFLAKE") {
Vector(
new Point(CENTER_X,70),
new Point(CENTER_X,580),
new Point(240,160),
new Point(760,490),
new Point(240,490),
new Point(760,160)
)
} else {
ellipse(
220,
160
)
}
}
// ============================================================
// OBJECT LIBRARY
// ============================================================
val objects =
Array(
"CAR",
"SPORTS CAR",
"TRUCK",
"BUS",
"TRAIN",
"AIRPLANE",
"JET",
"HELICOPTER",
"ROCKET",
"BICYCLE",
"MOTORBIKE",
"BOAT",
"SHIP",
"SUBMARINE",
"DRONE",
"UFO",
"HOUSE",
"VILLA",
"CASTLE",
"TOWER",
"BRIDGE",
"CITY",
"TREE",
"PALM TREE",
"FLOWER",
"CACTUS",
"MOUNTAIN",
"VOLCANO",
"ISLAND",
"RAINBOW",
"SUN",
"MOON",
"PLANET",
"EARTH",
"SATURN",
"BOY",
"GIRL",
"PERSON",
"ROBOT",
"ASTRONAUT",
"KING",
"QUEEN",
"CAT",
"DOG",
"BIRD",
"FISH",
"SHARK",
"WHALE",
"DOLPHIN",
"LION",
"TIGER",
"ELEPHANT",
"HORSE",
"RABBIT",
"PANDA",
"MONKEY",
"FOX",
"DEER",
"BUTTERFLY",
"BEE",
"SNAKE",
"TURTLE",
"APPLE",
"BANANA",
"MANGO",
"PIZZA",
"BURGER",
"CAKE",
"GUITAR",
"PIANO",
"CAMERA",
"COMPUTER",
"PHONE",
"BOOK",
"CLOCK",
"KEY",
"LAMP",
"CHAIR",
"TABLE",
"STAR",
"HEART",
"DIAMOND",
"CROWN",
"TROPHY",
"GIFT",
"BALLOON",
"UMBRELLA",
"FOOTBALL",
"BASKETBALL",
"CRICKET BAT",
"CRICKET BALL",
"FUTURE CAR",
"CYBER CITY",
"NEON TOWER",
"TIME MACHINE"
)
val objectModel =
new DefaultListModel[String]()
var objectInit: Int =
0
while (
objectInit < objects.length
) {
objectModel.addElement(
objects(objectInit)
)
objectInit += 1
}
// ============================================================
// OBJECT GEOMETRY
// ============================================================
def objectGeometry(
name: String
): Vector[Point] = {
val n =
name.toUpperCase
if (n.indexOf("CAR") >= 0) {
Vector(
new Point(100,470),
new Point(215,365),
new Point(390,355),
new Point(470,245),
new Point(660,245),
new Point(775,355),
new Point(900,370),
new Point(950,470),
new Point(100,470)
)
} else if (
n == "HOUSE" ||
n == "VILLA" ||
n == "CASTLE"
) {
Vector(
new Point(220,555),
new Point(220,300),
new Point(500,70),
new Point(780,300),
new Point(780,555),
new Point(220,555)
)
} else if (
n.indexOf("TREE") >= 0
) {
Vector(
new Point(455,580),
new Point(545,580),
new Point(545,350),
new Point(680,270),
new Point(600,150),
new Point(500,215),
new Point(400,150),
new Point(320,270),
new Point(455,350),
new Point(455,580)
)
} else if (n == "BIRD") {
Vector(
new Point(180,350),
new Point(365,215),
new Point(500,300),
new Point(660,180),
new Point(820,330),
new Point(630,415),
new Point(450,400),
new Point(310,470),
new Point(180,350)
)
} else if (
n == "FISH" ||
n == "SHARK" ||
n == "WHALE" ||
n == "DOLPHIN"
) {
Vector(
new Point(120,325),
new Point(300,210),
new Point(650,210),
new Point(865,325),
new Point(650,440),
new Point(300,440),
new Point(120,325)
)
} else if (
n.indexOf("ROCKET") >= 0 ||
n == "UFO"
) {
Vector(
new Point(425,525),
new Point(450,220),
new Point(500,60),
new Point(550,220),
new Point(575,525),
new Point(500,600),
new Point(425,525)
)
} else if (
n.indexOf("ROBOT") >= 0
) {
Vector(
new Point(350,555),
new Point(350,300),
new Point(330,300),
new Point(330,130),
new Point(670,130),
new Point(670,300),
new Point(650,300),
new Point(650,555),
new Point(350,555)
)
} else if (
n.indexOf("MOUNTAIN") >= 0
) {
Vector(
new Point(40,560),
new Point(280,205),
new Point(430,400),
new Point(620,75),
new Point(960,560),
new Point(40,560)
)
} else if (n == "FLOWER") {
Vector(
new Point(500,585),
new Point(500,315),
new Point(390,225),
new Point(500,115),
new Point(610,225),
new Point(500,315),
new Point(500,585)
)
} else if (n == "STAR") {
star(
5,
250
)
} else if (n == "HEART") {
heart()
} else if (n == "CROWN") {
Vector(
new Point(230,490),
new Point(195,155),
new Point(390,295),
new Point(500,80),
new Point(610,295),
new Point(805,155),
new Point(770,490),
new Point(230,490)
)
} else {
polygon(
6,
230,
0.0
)
}
}
// ============================================================
// ADD OBJECTS
// ============================================================
def addShape(
name: String
): Unit = {
saveHistory()
val pts =
shapeGeometry(name)
drawItems +=
DrawItem(
pts,
strokeColor,
fillColor,
closed(pts),
name,
true,
false
)
selectedIndex =
drawItems.length - 1
activeTool =
"SELECT"
repaintCanvas()
setStatus(
"SHAPE ADDED: " + name
)
}
def addObject(
name: String
): Unit = {
saveHistory()
val pts =
objectGeometry(name)
drawItems +=
DrawItem(
pts,
strokeColor,
fillColor,
closed(pts),
name,
true,
false
)
selectedIndex =
drawItems.length - 1
activeTool =
"SELECT"
repaintCanvas()
setStatus(
"OBJECT ADDED: " + name
)
}
// ============================================================
// FILL
// ============================================================
def fillSelected(): Unit = {
if (
selectedIndex >= 0 &&
selectedIndex < drawItems.length
) {
val item =
drawItems(selectedIndex)
if (
closed(item.points)
) {
saveHistory()
drawItems(selectedIndex) =
item.copy(
fill = fillColor,
filled = true
)
repaintCanvas()
setStatus(
"FILL COMPLETE"
)
} else {
setStatus(
"CLOSED SHAPE REQUIRED"
)
}
} else {
setStatus(
"SELECT OBJECT FIRST"
)
}
}
// ============================================================
// DELETE / CLEAR
// ============================================================
def deleteSelected(): Unit = {
if (
selectedIndex >= 0 &&
selectedIndex < drawItems.length
) {
saveHistory()
drawItems.remove(
selectedIndex
)
selectedIndex =
-1
repaintCanvas()
setStatus(
"OBJECT DELETED"
)
}
}
def clearAll(): Unit = {
if (drawItems.nonEmpty) {
saveHistory()
}
drawItems.clear()
tempPoints.clear()
selectedIndex =
-1
if (codeArea != null) {
codeArea.setText(
"// CANVAS CLEARED"
)
}
repaintCanvas()
setStatus(
"CANVAS CLEARED"
)
}
// ============================================================
// COLOR NAME
// ============================================================
def colorName(
c: Color
): String = {
val r =
c.getRed
val g =
c.getGreen
val b =
c.getBlue
if (
r > 220 &&
g < 100 &&
b < 100
) {
"red"
} else if (
r < 100 &&
g > 170 &&
b < 120
) {
"green"
} else if (
r < 100 &&
g < 160 &&
b > 170
) {
"blue"
} else if (
r > 220 &&
g > 170 &&
b < 130
) {
"yellow"
} else if (
r > 220 &&
g > 100 &&
b < 120
) {
"orange"
} else if (
r > 160 &&
b > 160
) {
"magenta"
} else if (
r > 170 &&
g > 170 &&
b > 170
) {
"white"
} else {
"black"
}
}
// ============================================================
// EXACT IMAGE IMPORT + CODE GENERATOR
// ============================================================
def escapeScalaString(text: String): String = {
if (text == null) ""
else text.replace("\\", "\\\\").replace("\"", "\\\"")
}
def imageToPngBytes(img: BufferedImage): Array[Byte] = {
val out = new ByteArrayOutputStream()
javax.imageio.ImageIO.write(img, "png", out)
out.toByteArray
}
def base64Lines(data: String, width: Int): String = {
val sb = new StringBuilder()
var i = 0
while (i < data.length) {
val end = math.min(i + width, data.length)
sb.append(data.substring(i, end))
if (end < data.length) sb.append("\n")
i = end
}
sb.toString
}
def generateExactImageCode(img: BufferedImage, title: String): String = {
if (img == null) return "// NO IMAGE AVAILABLE\n"
val png = imageToPngBytes(img)
val encoded = java.util.Base64.getEncoder.encodeToString(png)
val sb = new StringBuilder()
sb.append("// ULTRA LEGEND - EXACT EMBEDDED IMAGE CODE\n")
sb.append("// Source: " + escapeScalaString(title) + "\n")
sb.append("// Original PNG pixels are embedded as Base64; no source-file path is required.\n")
sb.append("// Viewer starts maximized and supports FIT / FILL / 100% / ZOOM.\n\n")
sb.append("import java.awt.BorderLayout\n")
sb.append("import java.awt.Color\n")
sb.append("import java.awt.Graphics2D\n")
sb.append("import java.awt.RenderingHints\n")
sb.append("import java.awt.event.MouseAdapter\n")
sb.append("import java.awt.event.MouseEvent\n")
sb.append("import java.awt.image.BufferedImage\n")
sb.append("import java.io.ByteArrayInputStream\n")
sb.append("import java.util.Base64\n")
sb.append("import javax.imageio.ImageIO\n")
sb.append("import javax.swing.JButton\n")
sb.append("import javax.swing.JFrame\n")
sb.append("import javax.swing.JLabel\n")
sb.append("import javax.swing.JPanel\n")
sb.append("import javax.swing.JScrollPane\n")
sb.append("import javax.swing.SwingConstants\n")
sb.append("import javax.swing.WindowConstants\n\n")
sb.append("val imageData = Array(\n")
var chunkIndex = 0
while (chunkIndex < encoded.length) {
val endChunk = math.min(chunkIndex + 120, encoded.length)
sb.append(" \"")
sb.append(encoded.substring(chunkIndex, endChunk))
sb.append("\"")
if (endChunk < encoded.length) sb.append(",")
sb.append("\n")
chunkIndex = endChunk
}
sb.append(").mkString\n")
sb.append("val image = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder.decode(imageData)))\n")
sb.append("var zoom = 1.0\n")
sb.append("var mode = \"FIT\"\n")
sb.append("val w = new JFrame(\"EXACT IMAGE VIEWER - " + escapeScalaString(title) + "\")\n")
sb.append("w.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)\n")
sb.append("w.setSize(1280, 820)\n")
sb.append("w.setLocationRelativeTo(null)\n")
sb.append("w.setExtendedState(JFrame.MAXIMIZED_BOTH)\n")
sb.append("val info = new JLabel(\"FIT\", SwingConstants.CENTER)\n")
sb.append("val view = new JPanel {\n")
sb.append(" override def paintComponent(graphics: java.awt.Graphics): Unit = {\n")
sb.append(" super.paintComponent(graphics)\n")
sb.append(" val g = graphics.asInstanceOf[Graphics2D]\n")
sb.append(" g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)\n")
sb.append(" g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)\n")
sb.append(" g.setColor(Color.BLACK)\n")
sb.append(" g.fillRect(0, 0, getWidth, getHeight)\n")
sb.append(" val baseScale = math.min(getWidth.toDouble / image.getWidth.toDouble, getHeight.toDouble / image.getHeight.toDouble)\n")
sb.append(" val scale = if (mode == \"FILL\") math.max(getWidth.toDouble / image.getWidth.toDouble, getHeight.toDouble / image.getHeight.toDouble) else if (mode == \"100%\") 1.0 else baseScale\n")
sb.append(" val finalScale = math.max(0.05, scale * zoom)\n")
sb.append(" val dw = math.max(1, math.round(image.getWidth * finalScale).toInt)\n")
sb.append(" val dh = math.max(1, math.round(image.getHeight * finalScale).toInt)\n")
sb.append(" val dx = (getWidth - dw) / 2\n")
sb.append(" val dy = (getHeight - dh) / 2\n")
sb.append(" g.drawImage(image, dx, dy, dw, dh, null)\n")
sb.append(" }\n")
sb.append("}\n")
sb.append("def setMode(m: String): Unit = { mode = m; zoom = 1.0; info.setText(m); view.repaint() }\n")
sb.append("val bar = new JPanel(new BorderLayout())\n")
sb.append("val left = new JPanel()\n")
sb.append("val fitB = new JButton(\"FIT\")\n")
sb.append("val fillB = new JButton(\"FILL\")\n")
sb.append("val oneB = new JButton(\"100%\")\n")
sb.append("val minusB = new JButton(\"ZOOM -\")\n")
sb.append("val plusB = new JButton(\"ZOOM +\")\n")
sb.append("fitB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = setMode(\"FIT\") })\n")
sb.append("fillB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = setMode(\"FILL\") })\n")
sb.append("oneB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = setMode(\"100%\") })\n")
sb.append("minusB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = { zoom = math.max(0.1, zoom - 0.1); view.repaint() } })\n")
sb.append("plusB.addActionListener(new javax.swing.AbstractAction { override def actionPerformed(e: java.awt.event.ActionEvent): Unit = { zoom = math.min(8.0, zoom + 0.1); view.repaint() } })\n")
sb.append("left.add(fitB); left.add(fillB); left.add(oneB); left.add(minusB); left.add(plusB)\n")
sb.append("bar.add(left, BorderLayout.WEST)\n")
sb.append("bar.add(info, BorderLayout.CENTER)\n")
sb.append("view.addMouseWheelListener(new MouseAdapter { override def mouseWheelMoved(e: java.awt.event.MouseWheelEvent): Unit = { zoom = math.max(0.1, math.min(8.0, zoom - e.getWheelRotation * 0.1)); view.repaint() } })\n")
sb.append("w.add(bar, BorderLayout.NORTH)\n")
sb.append("w.add(view, BorderLayout.CENTER)\n")
sb.append("w.setVisible(true)\n")
sb.toString
}
def isImageFile(file: java.io.File): Boolean = {
if (file == null) false
else {
val n = file.getName.toLowerCase
n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") ||
n.endsWith(".gif") || n.endsWith(".bmp")
}
}
def isVideoFile(file: java.io.File): Boolean = {
if (file == null) false
else {
val n = file.getName.toLowerCase
n.endsWith(".mp4") || n.endsWith(".mov") || n.endsWith(".avi") ||
n.endsWith(".mkv") || n.endsWith(".webm")
}
}
def importMediaFile(file: java.io.File): Unit = {
if (file == null || !file.exists() || !file.isFile) {
setStatus("IMAGE FILE NOT FOUND")
} else if (isImageFile(file)) {
importImageFile(file)
} else {
setStatus("IMAGE ONLY: PNG JPG JPEG GIF BMP")
}
}
def restoreImportedMediaCode(): Unit = {
if (importedImageOriginal != null) {
generatedCode = generateExactImageCode(importedImageOriginal, importedImageName)
if (codeArea != null) codeArea.setText(generatedCode)
setStatus("IMAGE CODE RESTORED: " + importedImageName)
} else {
setStatus("NO IMPORTED IMAGE TO RESTORE")
}
}
def bufferedImageFromAwtImage(source: java.awt.Image): BufferedImage = {
if (source == null) return null
val w = math.max(1, source.getWidth(null))
val h = math.max(1, source.getHeight(null))
val out = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB)
val g = out.createGraphics()
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
g.drawImage(source, 0, 0, null)
g.dispose()
out
}
def pasteImageFromClipboard(): Unit = {
try {
val cb = Toolkit.getDefaultToolkit.getSystemClipboard
val t = cb.getContents(null)
if (t != null && t.isDataFlavorSupported(DataFlavor.imageFlavor)) {
val raw = t.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
val img = bufferedImageFromAwtImage(raw)
if (img != null) {
importedImageOriginal = img
importedImage = fitImageToCanvas(img)
importedImagePath = "CLIPBOARD"
importedImageName = "Clipboard Image"
generatedCode = generateExactImageCode(importedImageOriginal, importedImageName)
generatedAnimationCode = ""
if (codeArea != null) codeArea.setText(generatedCode)
repaintCanvas()
setStatus("IMAGE PASTED FROM CLIPBOARD: " + img.getWidth + "x" + img.getHeight)
} else {
setStatus("CLIPBOARD IMAGE NOT AVAILABLE")
}
return
}
setStatus("CLIPBOARD DOES NOT CONTAIN AN IMAGE")
} catch {
case ex: Throwable => setStatus("PASTE IMAGE ERROR: " + ex.getMessage)
}
}
def imageDropHandler(): TransferHandler = {
new TransferHandler() {
override def canImport(support: TransferHandler.TransferSupport): Boolean = {
support.isDataFlavorSupported(DataFlavor.imageFlavor) ||
support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
}
override def importData(support: TransferHandler.TransferSupport): Boolean = {
try {
if (!canImport(support)) return false
val tr = support.getTransferable
if (tr.isDataFlavorSupported(DataFlavor.imageFlavor)) {
val raw = tr.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
val img = bufferedImageFromAwtImage(raw)
if (img != null) {
importedImageOriginal = img
importedImage = fitImageToCanvas(img)
importedImagePath = "DRAGGED IMAGE"
importedImageName = "Dropped Image"
generatedCode = generateExactImageCode(importedImageOriginal, importedImageName)
generatedAnimationCode = ""
if (codeArea != null) codeArea.setText(generatedCode)
repaintCanvas()
setStatus("IMAGE DROPPED INTO PAINT + EXACT CODE READY")
true
} else false
} else {
val data = tr.getTransferData(DataFlavor.javaFileListFlavor)
val files = data.asInstanceOf[java.util.List[_]]
if (files == null || files.isEmpty) return false
val it = files.iterator()
if (it.hasNext) {
val f = it.next().asInstanceOf[java.io.File]
if (isImageFile(f)) {
importImageFile(f)
true
} else {
setStatus("PAINT ACCEPTS IMAGES ONLY")
false
}
} else false
}
} catch {
case ex: Throwable =>
setStatus("PAINT DROP ERROR: " + ex.getMessage)
false
}
}
}
}
def mediaDropHandler(): TransferHandler = {
new TransferHandler() {
override def canImport(support: TransferHandler.TransferSupport): Boolean = {
support.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ||
support.isDataFlavorSupported(DataFlavor.imageFlavor)
}
override def importData(support: TransferHandler.TransferSupport): Boolean = {
try {
if (!canImport(support)) return false
val tr = support.getTransferable
if (tr.isDataFlavorSupported(DataFlavor.imageFlavor)) {
val raw = tr.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
val img = bufferedImageFromAwtImage(raw)
if (img != null) {
importedImageOriginal = img
importedImage = fitImageToCanvas(img)
importedImagePath = "DRAGGED CLIPBOARD IMAGE"
importedImageName = "Dropped Image"
generatedCode = generateExactImageCode(importedImageOriginal, importedImageName)
if (codeArea != null) codeArea.setText(generatedCode)
repaintCanvas()
setStatus("IMAGE DROPPED + EXACT CODE READY")
true
} else false
} else {
val data = tr.getTransferData(DataFlavor.javaFileListFlavor)
val files = data.asInstanceOf[java.util.List[_]]
if (files == null || files.isEmpty) return false
val it = files.iterator()
if (it.hasNext) {
val f = it.next().asInstanceOf[java.io.File]
importMediaFile(f)
true
} else false
}
} catch {
case ex: Throwable =>
setStatus("DROP IMPORT ERROR: " + ex.getMessage)
false
}
}
}
}
def fitImageToCanvas(img: BufferedImage): BufferedImage = {
if (img == null) return null
val fit = math.min(
CANVAS_W.toDouble / img.getWidth.toDouble,
CANVAS_H.toDouble / img.getHeight.toDouble
)
val drawW = math.max(1, math.round(img.getWidth * fit).toInt)
val drawH = math.max(1, math.round(img.getHeight * fit).toInt)
val scaled = new BufferedImage(drawW, drawH, BufferedImage.TYPE_INT_ARGB)
val sg = scaled.createGraphics()
sg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
sg.drawImage(img, 0, 0, drawW, drawH, null)
sg.dispose()
scaled
}
def importImageFile(file: java.io.File): Unit = {
try {
val img = javax.imageio.ImageIO.read(file)
if (img == null) {
setStatus("NOT A SUPPORTED IMAGE")
return
}
importedImageOriginal = img
importedImage = img
importedImagePath = file.getAbsolutePath
importedImageName = file.getName
importedImage = fitImageToCanvas(img)
repaintCanvas()
if (codeArea != null) codeArea.setText(generateExactImageCode(img, file.getName))
generatedCode = generateExactImageCode(img, file.getName)
setStatus("IMAGE IMPORTED EXACTLY: " + file.getName + " | " + img.getWidth + "x" + img.getHeight)
} catch {
case ex: Throwable => setStatus("IMAGE IMPORT ERROR: " + ex.getMessage)
}
}
def chooseAndImportImage(parent: JFrame): Unit = {
val chooser = new JFileChooser(defaultMediaDirectory())
chooser.setDialogTitle("IMPORT IMAGE FROM FILES")
chooser.setFileFilter(new FileNameExtensionFilter("Images: PNG, JPG, JPEG, GIF, BMP", "png", "jpg", "jpeg", "gif", "bmp"))
if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
importImageFile(chooser.getSelectedFile)
}
}
// ============================================================
// GENERATE KOJO CODE
// ============================================================
def generateCode(): Unit = {
if (importedImageOriginal != null) {
generatedCode = generateExactImageCode(
importedImageOriginal,
importedImageName
)
codeArea.setText(generatedCode)
setStatus("EXACT IMAGE CODE GENERATED: " + importedImageOriginal.getWidth + "x" + importedImageOriginal.getHeight)
return
}
val sb =
new StringBuilder()
sb.append(
"// ULTRA LEGEND GENERATED KOJO CODE\n"
)
sb.append(
"cleari()\n"
)
sb.append(
"setAnimationDelay(5)\n\n"
)
if (drawItems.isEmpty) {
sb.append(
"// NOTHING DRAWN\n"
)
} else {
var i: Int =
0
while (
i < drawItems.length
) {
val item =
drawItems(i)
if (
item.visible &&
item.points.nonEmpty
) {
sb.append(
"// " +
item.name +
"\n"
)
sb.append(
"setPenColor(" +
colorName(
item.stroke
) +
")\n"
)
sb.append(
"penUp()\n"
)
sb.append(
"setPosition(" +
(
item.points.head.x -
CENTER_X
) +
", " +
(
CENTER_Y -
item.points.head.y
) +
")\n"
)
sb.append(
"penDown()\n"
)
var j: Int =
1
while (
j < item.points.length
) {
sb.append(
"lineTo(" +
(
item.points(j).x -
CENTER_X
) +
", " +
(
CENTER_Y -
item.points(j).y
) +
")\n"
)
j += 1
}
if (item.filled && item.points.length >= 3) {
sb.append(
"setFillColor(" +
item.fill.getRed + "," +
item.fill.getGreen + "," +
item.fill.getBlue +
")\n"
)
sb.append(
"// Fill RGB: " +
item.fill.getRed + "," +
item.fill.getGreen + "," +
item.fill.getBlue +
"\n"
)
}
sb.append(
"\n"
)
}
i += 1
}
}
generatedCode =
sb.toString
codeArea.setText(
generatedCode
)
setStatus(
"DRAW CODE GENERATED"
)
}
// ============================================================
// COPY / SAVE
// ============================================================
def copyText(
text: String
): Unit = {
try {
val clipboard =
java.awt.Toolkit
.getDefaultToolkit
.getSystemClipboard
clipboard.setContents(
new java.awt.datatransfer.StringSelection(
text
),
null
)
setStatus(
"COPIED"
)
} catch {
case _: Exception =>
setStatus(
"COPY FAILED"
)
}
}
def saveText(
text: String
): Unit = {
val chooser =
new JFileChooser()
chooser.setDialogTitle(
"SAVE SCALA CODE"
)
if (
chooser.showSaveDialog(
mainFrame
) ==
JFileChooser.APPROVE_OPTION
) {
try {
var file =
chooser.getSelectedFile
if (
!file.getName
.toLowerCase
.endsWith(".scala")
) {
file =
new java.io.File(
file.getAbsolutePath +
".scala"
)
}
val out =
new java.io.PrintWriter(
file
)
out.write(text)
out.close()
setStatus(
"FILE SAVED"
)
} catch {
case _: Exception =>
setStatus(
"SAVE FAILED"
)
}
}
}
// ============================================================
// ANIMATION CODE
// ============================================================
def generateAnimationCode(): Unit = {
val sb =
new StringBuilder()
sb.append(
"// ULTRA LEGEND ANIMATION CODE\n"
)
sb.append(
"cleari()\n"
)
sb.append(
"val p = Picture.circle(40)\n"
)
sb.append(
"p.setPosition(0,0)\n"
)
sb.append(
"draw(p)\n\n"
)
sb.append(
"animate {\n"
)
sb.append(
" p.forward(3)\n"
)
sb.append(
" p.rotate(3)\n"
)
sb.append(
"}\n"
)
generatedAnimationCode =
sb.toString
codeArea.setText(
generatedAnimationCode
)
setStatus(
"ANIMATION CODE GENERATED"
)
}
// ============================================================
// GAME DATABASE
// ============================================================
val gameDatabase =
Array(
GameInfo(
"SLING BALL",
"MOUSE",
"Drag and release the ball toward the target."
),
GameInfo(
"PONG",
"MOUSE",
"Move the paddle and keep the ball alive."
),
GameInfo(
"SNAKE",
"KEYBOARD",
"Use arrows or WASD to collect food."
),
GameInfo(
"BREAKOUT",
"MOUSE",
"Move the paddle and break blocks."
),
GameInfo(
"FLAPPY BIRD",
"MOUSE",
"Click to flap and pass the obstacles."
),
GameInfo(
"CAR DODGE",
"KEYBOARD",
"Move left and right to avoid traffic."
),
GameInfo(
"SPACE SHOOTER",
"KEYBOARD",
"Move and fire at falling enemies."
),
GameInfo(
"TARGET RUSH",
"MOUSE",
"Click targets as quickly as possible."
),
GameInfo(
"BALL CATCH",
"MOUSE",
"Move the basket and catch falling balls."
),
GameInfo(
"MAZE BALL",
"MOUSE",
"Drag the ball through a maze."
),
GameInfo(
"TIC TAC TOE",
"MOUSE",
"Play against a simple computer."
),
GameInfo(
"KEYBOARD RUNNER",
"KEYBOARD",
"Run, jump, collect coins and avoid enemies."
),
GameInfo(
"KEYBOARD SPACE",
"KEYBOARD",
"Control a spaceship and shoot enemies."
),
GameInfo(
"PLATFORM QUEST",
"KEYBOARD",
"Jump, collect coins and reach the goal."
),
GameInfo(
"DODGE FIELD",
"MOUSE",
"Move around and avoid falling enemies."
)
)
// ============================================================
// GAME WINDOW
// ============================================================
def gameFrame(
title: String
): JFrame = {
val frame =
new JFrame(title)
frame.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE
)
frame.setSize(
900,
650
)
frame.setLocationRelativeTo(
mainFrame
)
frame
}
// ============================================================
// GAME 1 - SLING BALL
// ============================================================
def playSlingBall(): Unit = {
val frame =
gameFrame(
"SLING BALL - MOUSE"
)
var ballX: Double =
110.0
var ballY: Double =
510.0
var vx: Double =
0.0
var vy: Double =
0.0
var launched: Boolean =
false
var won: Boolean =
false
val target =
new java.awt.Rectangle(
760,
90,
70,
70
)
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setPaint(
new GradientPaint(
0,
0,
new Color(30,100,160),
0,
getHeight,
new Color(10,20,40)
)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.RED
)
g.fillOval(
target.x,
target.y,
target.width,
target.height
)
g.setColor(
Color.YELLOW
)
g.fillOval(
ballX.toInt - 10,
ballY.toInt - 10,
20,
20
)
g.setColor(
Color.WHITE
)
g.drawString(
if (won)
"YOU WIN!"
else
"MOVE MOUSE + RELEASE TO LAUNCH",
20,
30
)
}
}
panel.addMouseListener(
new MouseAdapter {
override def mouseReleased(
e: MouseEvent
): Unit = {
if (!launched) {
vx =
(
ballX -
e.getX
) * 0.08
vy =
(
ballY -
e.getY
) * 0.08
launched =
true
}
}
}
)
val timer: Timer =
new Timer(
25,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
if (
launched &&
!won
) {
vy +=
0.25
ballX +=
vx
ballY +=
vy
if (
ballX < 10 ||
ballX > frame.getWidth - 10
) {
vx =
-vx
}
if (ballY < 10) {
vy =
-vy
}
if (
ballY >
frame.getHeight + 20
) {
ballX =
110
ballY =
510
vx =
0
vy =
0
launched =
false
}
if (
target.contains(
ballX.toInt,
ballY.toInt
)
) {
won =
true
}
panel.repaint()
}
}
}
)
timer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
timer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
}
// ============================================================
// GAME 2 - PONG
// ============================================================
def playPong(): Unit = {
val frame =
gameFrame(
"PONG - MOUSE"
)
var paddleX: Double =
350.0
var ballX: Double =
420.0
var ballY: Double =
280.0
var vx: Double =
5.0
var vy: Double =
4.0
var score: Int =
0
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setColor(
Color.BLACK
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.WHITE
)
g.fillOval(
ballX.toInt - 10,
ballY.toInt - 10,
20,
20
)
g.fillRoundRect(
paddleX.toInt,
getHeight - 60,
150,
20,
10,
10
)
g.drawString(
"SCORE: " + score,
20,
30
)
}
}
panel.addMouseMotionListener(
new MouseMotionAdapter {
override def mouseMoved(
e: MouseEvent
): Unit = {
paddleX =
math.max(
0,
math.min(
panel.getWidth - 150,
e.getX - 75
)
)
}
}
)
val pongTimer: Timer =
new Timer(
25,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
ballX +=
vx
ballY +=
vy
if (
ballX < 10 ||
ballX > panel.getWidth - 10
) {
vx =
-vx
}
if (ballY < 10) {
vy =
-vy
}
if (
ballY >
panel.getHeight - 85 &&
ballX >= paddleX &&
ballX <= paddleX + 150
) {
vy =
-math.abs(vy)
score +=
1
}
if (
ballY >
panel.getHeight
) {
ballX =
panel.getWidth / 2.0
ballY =
panel.getHeight / 2.0
vx =
5.0
vy =
-4.0
score =
0
}
panel.repaint()
}
}
)
pongTimer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
pongTimer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
}
// ============================================================
// GAME 3 - KEYBOARD SNAKE
// ============================================================
def playSnake(): Unit = {
val frame =
gameFrame(
"SNAKE - KEYBOARD"
)
val cell: Int =
25
val body =
ArrayBuffer[Point](
new Point(10,10),
new Point(9,10),
new Point(8,10)
)
var food =
new Point(
16,
10
)
var dirX: Int =
1
var dirY: Int =
0
var score: Int =
0
var running: Boolean =
true
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setColor(
new Color(15,50,25)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
new Color(
255,
255,
255,
20
)
)
var gx: Int =
0
while (
gx <= getWidth
) {
g.drawLine(
gx,
0,
gx,
getHeight
)
gx +=
cell
}
var gy: Int =
0
while (
gy <= getHeight
) {
g.drawLine(
0,
gy,
getWidth,
gy
)
gy +=
cell
}
g.setColor(
Color.RED
)
g.fillOval(
food.x * cell + 3,
food.y * cell + 3,
cell - 6,
cell - 6
)
var i: Int =
0
while (
i < body.length
) {
g.setColor(
if (i == 0)
new Color(80,250,100)
else
new Color(35,175,65)
)
g.fillRoundRect(
body(i).x * cell + 2,
body(i).y * cell + 2,
cell - 4,
cell - 4,
8,
8
)
i +=
1
}
g.setColor(
Color.WHITE
)
g.setFont(
new Font(
"Arial",
Font.BOLD,
18
)
)
g.drawString(
"ARROWS / WASD SCORE: " + score,
15,
25
)
if (!running) {
g.setFont(
new Font(
"Arial",
Font.BOLD,
42
)
)
g.drawString(
"GAME OVER",
290,
300
)
}
}
}
panel.setFocusable(
true
)
panel.addKeyListener(
new KeyAdapter {
override def keyPressed(
e: KeyEvent
): Unit = {
val k =
e.getKeyCode
if (
k == KeyEvent.VK_LEFT ||
k == KeyEvent.VK_A
) {
if (dirX != 1) {
dirX =
-1
dirY =
0
}
} else if (
k == KeyEvent.VK_RIGHT ||
k == KeyEvent.VK_D
) {
if (dirX != -1) {
dirX =
1
dirY =
0
}
} else if (
k == KeyEvent.VK_UP ||
k == KeyEvent.VK_W
) {
if (dirY != 1) {
dirX =
0
dirY =
-1
}
} else if (
k == KeyEvent.VK_DOWN ||
k == KeyEvent.VK_S
) {
if (dirY != -1) {
dirX =
0
dirY =
1
}
}
}
}
)
val snakeTimer: Timer =
new Timer(
110,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
if (
running &&
body.nonEmpty
) {
val head =
body.head
val nx =
head.x +
dirX
val ny =
head.y +
dirY
val cols =
math.max(
1,
panel.getWidth / cell
)
val rows =
math.max(
1,
panel.getHeight / cell
)
if (
nx < 0 ||
ny < 0 ||
nx >= cols ||
ny >= rows
) {
running =
false
} else {
val newHead =
new Point(
nx,
ny
)
var hitSelf: Boolean =
false
var i: Int =
0
while (
i < body.length
) {
if (
body(i).x == newHead.x &&
body(i).y == newHead.y
) {
hitSelf =
true
}
i +=
1
}
if (hitSelf) {
running =
false
} else {
body.prepend(
newHead
)
if (
newHead.x == food.x &&
newHead.y == food.y
) {
score +=
1
var foundFood: Boolean =
false
while (!foundFood) {
val fx =
(
RANDOM_GEN.nextDouble() *
cols
).toInt
val fy =
(
RANDOM_GEN.nextDouble() *
rows
).toInt
foundFood =
true
var q: Int =
0
while (
q < body.length
) {
if (
body(q).x == fx &&
body(q).y == fy
) {
foundFood =
false
}
q +=
1
}
if (foundFood) {
food =
new Point(
fx,
fy
)
}
}
} else {
body.remove(
body.length - 1
)
}
}
}
panel.repaint()
}
}
}
)
snakeTimer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
snakeTimer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
panel.requestFocusInWindow()
}
// ============================================================
// GAME 4 - BREAKOUT
// ============================================================
def playBreakout(): Unit = {
val frame =
gameFrame(
"BREAKOUT - MOUSE"
)
var paddleX: Double =
350.0
var bx: Double =
450.0
var by: Double =
420.0
var vx: Double =
4.0
var vy: Double =
-4.0
var score: Int =
0
val blocks =
ArrayBuffer[Point]()
var r: Int =
0
while (
r < 5
) {
var c: Int =
0
while (
c < 10
) {
blocks +=
new Point(
40 + c * 75,
70 + r * 32
)
c +=
1
}
r +=
1
}
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setColor(
new Color(18,18,40)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.WHITE
)
g.fillOval(
bx.toInt - 9,
by.toInt - 9,
18,
18
)
g.setColor(
Color.CYAN
)
g.fillRoundRect(
paddleX.toInt,
getHeight - 55,
150,
18,
10,
10
)
g.setColor(
Color.ORANGE
)
var i: Int =
0
while (
i < blocks.length
) {
g.fillRect(
blocks(i).x,
blocks(i).y,
65,
22
)
i +=
1
}
g.setColor(
Color.WHITE
)
g.drawString(
"SCORE: " + score,
20,
28
)
}
}
panel.addMouseMotionListener(
new MouseMotionAdapter {
override def mouseMoved(
e: MouseEvent
): Unit = {
paddleX =
math.max(
0,
math.min(
panel.getWidth - 150,
e.getX - 75
)
)
}
}
)
val breakoutTimer: Timer =
new Timer(
25,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
bx +=
vx
by +=
vy
if (
bx < 10 ||
bx > panel.getWidth - 10
) {
vx =
-vx
}
if (by < 10) {
vy =
-vy
}
if (
by >
panel.getHeight - 75 &&
bx >= paddleX &&
bx <= paddleX + 150
) {
vy =
-math.abs(vy)
}
var hitBlock: Int =
-1
var i: Int =
0
while (
i < blocks.length
) {
if (
bx >= blocks(i).x &&
bx <= blocks(i).x + 65 &&
by >= blocks(i).y &&
by <= blocks(i).y + 22
) {
hitBlock =
i
}
i +=
1
}
if (
hitBlock >= 0
) {
blocks.remove(
hitBlock
)
vy =
-vy
score +=
1
}
if (
by >
panel.getHeight
) {
bx =
panel.getWidth / 2.0
by =
panel.getHeight / 2.0
vx =
4.0
vy =
-4.0
score =
0
}
panel.repaint()
}
}
)
breakoutTimer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
breakoutTimer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
}
// ============================================================
// GAME 5 - FLAPPY
// ============================================================
def playFlappy(): Unit = {
val frame =
gameFrame(
"FLAPPY BIRD - MOUSE"
)
var birdY: Double =
280.0
var birdV: Double =
0.0
var pipeX: Double =
850.0
var gap: Double =
290.0
var score: Int =
0
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setPaint(
new GradientPaint(
0,
0,
new Color(70,190,255),
0,
getHeight,
Color.WHITE
)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.YELLOW
)
g.fillOval(
150,
birdY.toInt,
45,
35
)
g.setColor(
Color.GREEN
)
g.fillRect(
pipeX.toInt,
0,
70,
gap.toInt - 85
)
g.fillRect(
pipeX.toInt,
gap.toInt + 85,
70,
getHeight
)
g.setColor(
Color.BLACK
)
g.drawString(
"CLICK TO FLAP SCORE: " + score,
20,
30
)
}
}
panel.addMouseListener(
new MouseAdapter {
override def mousePressed(
e: MouseEvent
): Unit = {
birdV =
-9.0
}
}
)
val flappyTimer: Timer =
new Timer(
25,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
birdV +=
0.4
birdY +=
birdV
pipeX -=
5.0
if (
pipeX <
-80
) {
pipeX =
panel.getWidth + 20
gap =
170 +
RANDOM_GEN.nextDouble() *
220
score +=
1
}
if (
birdY < 0 ||
birdY > panel.getHeight
) {
birdY =
280
birdV =
0
pipeX =
panel.getWidth
score =
0
}
panel.repaint()
}
}
)
flappyTimer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
flappyTimer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
}
// ============================================================
// GAME 6 - KEYBOARD CAR
// ============================================================
def playKeyboardCar(): Unit = {
val frame =
gameFrame(
"CAR DODGE - KEYBOARD"
)
var playerX: Double =
420.0
var enemyX: Double =
240.0
var enemyY: Double =
-120.0
var score: Int =
0
var running: Boolean =
true
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setColor(
new Color(45,45,50)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.WHITE
)
var roadY: Int =
0
while (
roadY < getHeight
) {
g.fillRect(
getWidth / 2 - 5,
roadY,
10,
60
)
roadY +=
100
}
g.setColor(
Color.BLUE
)
g.fillRoundRect(
playerX.toInt,
getHeight - 125,
60,
100,
18,
18
)
g.setColor(
Color.RED
)
g.fillRoundRect(
enemyX.toInt,
enemyY.toInt,
60,
100,
18,
18
)
g.setColor(
Color.WHITE
)
g.drawString(
"LEFT / RIGHT SCORE: " + score,
15,
28
)
if (!running) {
g.setFont(
new Font(
"Arial",
Font.BOLD,
42
)
)
g.drawString(
"GAME OVER",
300,
300
)
}
}
}
panel.setFocusable(
true
)
panel.addKeyListener(
new KeyAdapter {
override def keyPressed(
e: KeyEvent
): Unit = {
val k =
e.getKeyCode
if (
k == KeyEvent.VK_LEFT ||
k == KeyEvent.VK_A
) {
playerX -=
15
} else if (
k == KeyEvent.VK_RIGHT ||
k == KeyEvent.VK_D
) {
playerX +=
15
}
playerX =
math.max(
0,
math.min(
panel.getWidth - 60,
playerX
)
)
}
}
)
val carTimer: Timer =
new Timer(
25,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
if (running) {
enemyY +=
7
if (
enemyY >
panel.getHeight
) {
enemyY =
-100
enemyX =
RANDOM_GEN.nextDouble() *
math.max(
1,
panel.getWidth - 60
)
score +=
1
}
val hit =
enemyX + 60 >
playerX &&
enemyX <
playerX + 60 &&
enemyY + 100 >
panel.getHeight - 125 &&
enemyY <
panel.getHeight
if (hit) {
running =
false
}
panel.repaint()
}
}
}
)
carTimer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
carTimer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
panel.requestFocusInWindow()
}
// ============================================================
// GAME 7 - KEYBOARD SPACE
// ============================================================
def playKeyboardSpace(): Unit = {
val frame =
gameFrame(
"SPACE SHOOTER - KEYBOARD"
)
var shipX: Double =
420.0
var enemyX: Double =
350.0
var enemyY: Double =
60.0
var laserX: Double =
-100.0
var laserY: Double =
-100.0
var score: Int =
0
var running: Boolean =
true
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setColor(
new Color(5,10,30)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.WHITE
)
var s: Int =
0
while (
s < 90
) {
g.fillOval(
(
s * 83
) %
math.max(
1,
getWidth
),
(
s * 47
) %
math.max(
1,
getHeight
),
2,
2
)
s +=
1
}
g.setColor(
Color.CYAN
)
val ship =
new java.awt.Polygon()
ship.addPoint(
shipX.toInt,
getHeight - 70
)
ship.addPoint(
shipX.toInt + 25,
getHeight - 130
)
ship.addPoint(
shipX.toInt + 50,
getHeight - 70
)
g.fillPolygon(
ship
)
g.setColor(
Color.RED
)
g.fillOval(
enemyX.toInt,
enemyY.toInt,
45,
35
)
if (
laserY >
-40
) {
g.setColor(
Color.YELLOW
)
g.fillRect(
laserX.toInt,
laserY.toInt,
6,
20
)
}
g.setColor(
Color.WHITE
)
g.drawString(
"LEFT / RIGHT + SPACE SCORE: " +
score,
15,
28
)
if (!running) {
g.setFont(
new Font(
"Arial",
Font.BOLD,
38
)
)
g.drawString(
"MISSION OVER",
285,
300
)
}
}
}
panel.setFocusable(
true
)
panel.addKeyListener(
new KeyAdapter {
override def keyPressed(
e: KeyEvent
): Unit = {
val k =
e.getKeyCode
if (
k == KeyEvent.VK_LEFT ||
k == KeyEvent.VK_A
) {
shipX -=
12
} else if (
k == KeyEvent.VK_RIGHT ||
k == KeyEvent.VK_D
) {
shipX +=
12
} else if (
k == KeyEvent.VK_SPACE
) {
laserX =
shipX + 22
laserY =
panel.getHeight - 150
}
shipX =
math.max(
0,
math.min(
panel.getWidth - 50,
shipX
)
)
}
}
)
val spaceTimer: Timer =
new Timer(
25,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
if (running) {
enemyY +=
2
laserY -=
12
if (
laserX >= enemyX &&
laserX <= enemyX + 45 &&
laserY <= enemyY + 35 &&
laserY >= enemyY - 20
) {
score +=
1
enemyX =
RANDOM_GEN.nextDouble() *
math.max(
1,
panel.getWidth - 45
)
enemyY =
50
}
if (
enemyY >
panel.getHeight
) {
enemyY =
50
}
panel.repaint()
}
}
}
)
spaceTimer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
spaceTimer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
panel.requestFocusInWindow()
}
// ============================================================
// GAME 8 - TARGET RUSH
// ============================================================
def playTargetRush(): Unit = {
val frame =
gameFrame(
"TARGET RUSH - MOUSE"
)
var tx: Double =
350.0
var ty: Double =
250.0
var score: Int =
0
var timeLeft: Int =
20
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setColor(
new Color(20,25,60)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.RED
)
g.fillOval(
tx.toInt,
ty.toInt,
80,
80
)
g.setColor(
Color.WHITE
)
g.drawOval(
tx.toInt + 18,
ty.toInt + 18,
44,
44
)
g.setFont(
new Font(
"Arial",
Font.BOLD,
20
)
)
g.drawString(
"SCORE: " +
score +
" TIME: " +
timeLeft,
20,
30
)
}
}
panel.addMouseListener(
new MouseAdapter {
override def mousePressed(
e: MouseEvent
): Unit = {
if (
e.getX >= tx &&
e.getX <= tx + 80 &&
e.getY >= ty &&
e.getY <= ty + 80 &&
timeLeft > 0
) {
score +=
1
tx =
RANDOM_GEN.nextDouble() *
math.max(
1,
panel.getWidth - 80
)
ty =
70 +
RANDOM_GEN.nextDouble() *
math.max(
1,
panel.getHeight - 150
)
panel.repaint()
}
}
}
)
val targetTimer: Timer =
new Timer(
1000,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
timeLeft -=
1
if (
timeLeft <=
0
) {
timeLeft =
20
score =
0
}
panel.repaint()
}
}
)
targetTimer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
targetTimer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
}
// ============================================================
// GAME 9 - BALL CATCH
// ============================================================
def playBallCatch(): Unit = {
val frame =
gameFrame(
"BALL CATCH - MOUSE"
)
var basketX: Double =
350.0
var ballX: Double =
200.0
var ballY: Double =
-50.0
var score: Int =
0
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setPaint(
new GradientPaint(
0,
0,
new Color(25,120,220),
0,
getHeight,
new Color(170,235,255)
)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.YELLOW
)
g.fillOval(
ballX.toInt,
ballY.toInt,
35,
35
)
g.setColor(
new Color(150,85,20)
)
g.fillRoundRect(
basketX.toInt,
getHeight - 80,
150,
35,
15,
15
)
g.setColor(
Color.WHITE
)
g.drawString(
"SCORE: " + score,
20,
30
)
}
}
panel.addMouseMotionListener(
new MouseMotionAdapter {
override def mouseMoved(
e: MouseEvent
): Unit = {
basketX =
math.max(
0,
math.min(
panel.getWidth - 150,
e.getX - 75
)
)
}
}
)
val catchTimer: Timer =
new Timer(
30,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
ballY +=
6
if (
ballY >
panel.getHeight - 110
) {
if (
ballX + 35 >= basketX &&
ballX <= basketX + 150
) {
score +=
1
}
ballX =
RANDOM_GEN.nextDouble() *
math.max(
1,
panel.getWidth - 35
)
ballY =
-40
}
panel.repaint()
}
}
)
catchTimer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
catchTimer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
}
// ============================================================
// GAME 10 - MAZE BALL
// ============================================================
def playMazeBall(): Unit = {
val frame =
gameFrame(
"MAZE BALL - MOUSE"
)
var ballX: Double =
70.0
var ballY: Double =
70.0
val target =
new java.awt.Rectangle(
730,
500,
60,
60
)
val walls =
Array(
new java.awt.Rectangle(
170,
90,
520,
20
),
new java.awt.Rectangle(
170,
90,
20,
340
),
new java.awt.Rectangle(
420,
220,
20,
330
),
new java.awt.Rectangle(
680,
90,
20,
430
)
)
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setColor(
Color.WHITE
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
new Color(75,75,85)
)
var i: Int =
0
while (
i < walls.length
) {
g.fillRect(
walls(i).x,
walls(i).y,
walls(i).width,
walls(i).height
)
i +=
1
}
g.setColor(
Color.GREEN
)
g.fillOval(
target.x,
target.y,
target.width,
target.height
)
g.setColor(
Color.RED
)
g.fillOval(
ballX.toInt,
ballY.toInt,
35,
35
)
g.setColor(
Color.BLACK
)
g.drawString(
"DRAG RED BALL TO GREEN TARGET",
20,
30
)
}
}
panel.addMouseMotionListener(
new MouseMotionAdapter {
override def mouseDragged(
e: MouseEvent
): Unit = {
val nx =
e.getX - 17
val ny =
e.getY - 17
val probe =
new java.awt.Rectangle(
nx,
ny,
35,
35
)
var hitWall: Boolean =
false
var i: Int =
0
while (
i < walls.length
) {
if (
walls(i).intersects(
probe
)
) {
hitWall =
true
}
i +=
1
}
if (!hitWall) {
ballX =
math.max(
0,
math.min(
panel.getWidth - 35,
nx
)
)
ballY =
math.max(
0,
math.min(
panel.getHeight - 35,
ny
)
)
}
if (
target.contains(
ballX.toInt + 17,
ballY.toInt + 17
)
) {
JOptionPane.showMessageDialog(
frame,
"YOU WIN!"
)
}
panel.repaint()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
}
// ============================================================
// GAME 11 - TIC TAC TOE
// ============================================================
def playTicTacToe(): Unit = {
val frame =
gameFrame(
"TIC TAC TOE"
)
val board =
Array.fill[Int](
9
)(
0
)
var humanTurn: Boolean =
true
var finished: Boolean =
false
def win(
player: Int
): Boolean = {
val lines =
Array(
Array(0,1,2),
Array(3,4,5),
Array(6,7,8),
Array(0,3,6),
Array(1,4,7),
Array(2,5,8),
Array(0,4,8),
Array(2,4,6)
)
var result: Boolean =
false
var i: Int =
0
while (
i < lines.length
) {
if (
board(lines(i)(0)) == player &&
board(lines(i)(1)) == player &&
board(lines(i)(2)) == player
) {
result =
true
}
i +=
1
}
result
}
def full(): Boolean = {
var result: Boolean =
true
var i: Int =
0
while (
i < 9
) {
if (
board(i) == 0
) {
result =
false
}
i +=
1
}
result
}
def computerMove(): Int = {
var chosen: Int =
-1
var i: Int =
0
while (
i < 9 &&
chosen < 0
) {
if (
board(i) == 0
) {
chosen =
i
}
i +=
1
}
chosen
}
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setColor(
new Color(
245,
248,
255
)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.BLACK
)
g.setStroke(
new BasicStroke(
6
)
)
var i: Int =
1
while (
i < 3
) {
g.drawLine(
i * getWidth / 3,
0,
i * getWidth / 3,
getHeight
)
g.drawLine(
0,
i * getHeight / 3,
getWidth,
i * getHeight / 3
)
i +=
1
}
g.setFont(
new Font(
"Arial",
Font.BOLD,
82
)
)
i =
0
while (
i < 9
) {
if (
board(i) != 0
) {
g.setColor(
if (
board(i) == 1
)
Color.BLUE
else
Color.RED
)
val col =
i % 3
val row =
i / 3
g.drawString(
if (
board(i) == 1
)
"X"
else
"O",
col *
getWidth / 3 +
65,
row *
getHeight / 3 +
105
)
}
i +=
1
}
g.setColor(
Color.DARK_GRAY
)
g.setFont(
new Font(
"Arial",
Font.BOLD,
18
)
)
g.drawString(
if (finished)
"GAME OVER"
else if (humanTurn)
"YOUR TURN"
else
"COMPUTER TURN",
20,
28
)
}
}
panel.addMouseListener(
new MouseAdapter {
override def mousePressed(
e: MouseEvent
): Unit = {
if (
humanTurn &&
!finished
) {
val cellW =
math.max(
1,
panel.getWidth / 3
)
val cellH =
math.max(
1,
panel.getHeight / 3
)
val col =
math.max(
0,
math.min(
2,
e.getX / cellW
)
)
val row =
math.max(
0,
math.min(
2,
e.getY / cellH
)
)
val idx =
row * 3 + col
if (
board(idx) == 0
) {
board(idx) =
1
if (
win(1) ||
full()
) {
finished =
true
} else {
humanTurn =
false
val ai =
computerMove()
if (
ai >= 0
) {
board(ai) =
2
}
if (
win(2) ||
full()
) {
finished =
true
} else {
humanTurn =
true
}
}
panel.repaint()
}
}
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
}
// ============================================================
// GAME 12 - KEYBOARD RUNNER
// ============================================================
def playKeyboardRunner(): Unit = {
val frame =
gameFrame(
"KEYBOARD RUNNER"
)
var px: Double =
100.0
var py: Double =
430.0
var velY: Double =
0.0
var score: Int =
0
var jumping: Boolean =
false
var running: Boolean =
true
val coins =
ArrayBuffer[Point](
new Point(260,390),
new Point(470,320),
new Point(670,390),
new Point(820,280)
)
val enemies =
ArrayBuffer[Point](
new Point(530,450),
new Point(760,450)
)
val panel =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setPaint(
new GradientPaint(
0,
0,
new Color(70,180,250),
0,
getHeight,
new Color(210,240,255)
)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
new Color(65,180,75)
)
g.fillRect(
0,
getHeight - 100,
getWidth,
100
)
g.setColor(
Color.BLUE
)
g.fillRoundRect(
px.toInt,
py.toInt,
45,
65,
10,
10
)
g.setColor(
Color.YELLOW
)
var i: Int =
0
while (
i < coins.length
) {
g.fillOval(
coins(i).x,
coins(i).y,
25,
25
)
i +=
1
}
g.setColor(
Color.RED
)
i =
0
while (
i < enemies.length
) {
g.fillRect(
enemies(i).x,
enemies(i).y,
45,
45
)
i +=
1
}
g.setColor(
Color.WHITE
)
g.drawString(
"A/D or ARROWS SPACE = JUMP SCORE: " +
score,
15,
28
)
if (!running) {
g.setColor(
Color.BLACK
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
g.setColor(
Color.WHITE
)
g.setFont(
new Font(
"Arial",
Font.BOLD,
42
)
)
g.drawString(
"GAME OVER",
300,
300
)
}
}
}
panel.setFocusable(
true
)
panel.addKeyListener(
new KeyAdapter {
override def keyPressed(
e: KeyEvent
): Unit = {
if (!running) {
return
}
val k =
e.getKeyCode
if (
k == KeyEvent.VK_LEFT ||
k == KeyEvent.VK_A
) {
px -=
12
} else if (
k == KeyEvent.VK_RIGHT ||
k == KeyEvent.VK_D
) {
px +=
12
} else if (
k == KeyEvent.VK_SPACE ||
k == KeyEvent.VK_UP
) {
if (!jumping) {
velY =
-11
jumping =
true
}
}
px =
math.max(
0,
math.min(
panel.getWidth - 45,
px
)
)
}
}
)
val runnerTimer: Timer =
new Timer(
30,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
if (running) {
velY +=
0.55
py +=
velY
val ground =
panel.getHeight - 165
if (
py >= ground
) {
py =
ground
velY =
0
jumping =
false
}
var i: Int =
coins.length - 1
while (
i >= 0
) {
val c =
coins(i)
val got =
px + 45 > c.x &&
px < c.x + 25 &&
py + 65 > c.y &&
py < c.y + 25
if (got) {
coins.remove(i)
score +=
10
}
i -=
1
}
i =
0
while (
i < enemies.length
) {
enemies(i).x -=
3
if (
enemies(i).x < -60
) {
enemies(i).x =
panel.getWidth +
100 +
i * 200
}
val hit =
px + 45 > enemies(i).x &&
px < enemies(i).x + 45 &&
py + 65 > enemies(i).y &&
py < enemies(i).y + 45
if (hit) {
running =
false
}
i +=
1
}
panel.repaint()
}
}
}
)
runnerTimer.start()
frame.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
runnerTimer.stop()
}
}
)
frame.add(
panel
)
frame.setVisible(
true
)
panel.requestFocusInWindow()
}
// ============================================================
// GAME ROUTER
// ============================================================
def launchGame(
name: String
): Unit = {
if (
name == "SLING BALL"
) {
playSlingBall()
} else if (
name == "PONG"
) {
playPong()
} else if (
name == "SNAKE"
) {
playSnake()
} else if (
name == "BREAKOUT"
) {
playBreakout()
} else if (
name == "FLAPPY BIRD"
) {
playFlappy()
} else if (
name == "CAR DODGE"
) {
playKeyboardCar()
} else if (
name == "SPACE SHOOTER"
) {
playKeyboardSpace()
} else if (
name == "TARGET RUSH"
) {
playTargetRush()
} else if (
name == "BALL CATCH"
) {
playBallCatch()
} else if (
name == "MAZE BALL"
) {
playMazeBall()
} else if (
name == "TIC TAC TOE"
) {
playTicTacToe()
} else {
playKeyboardRunner()
}
}
// ============================================================
// GAME STUDIO
// ============================================================
def openGameStudio(): Unit = {
val frame =
new JFrame(
"ULTRA LEGEND GAME STUDIO"
)
frame.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE
)
frame.setSize(
1250,
780
)
frame.setLocationRelativeTo(
mainFrame
)
val model =
new DefaultListModel[String]()
var i: Int =
0
while (
i < gameDatabase.length
) {
model.addElement(
gameDatabase(i).name
)
i +=
1
}
val list =
new JList[String](
model
)
list.setSelectionMode(
ListSelectionModel.SINGLE_SELECTION
)
list.setFont(
new Font(
"Arial",
Font.BOLD,
14
)
)
val search =
new JTextField(
14
)
val searchButton =
button(
"SEARCH"
)
val playButton =
button(
"PLAY"
)
val codeButton =
button(
"GAME CODE"
)
val closeButton =
button(
"CLOSE"
)
val info =
new JTextArea()
info.setEditable(
false
)
info.setFont(
new Font(
"Monospaced",
Font.PLAIN,
13
)
)
info.setText(
"ULTRA LEGEND GAME STUDIO\n\n" +
"MOUSE + KEYBOARD GAMES\n\n" +
"DOUBLE CLICK A GAME TO PLAY.\n" +
"SELECT GAME AND PRESS GAME CODE.\n"
)
val searchPanel =
new JPanel(
new FlowLayout(
FlowLayout.LEFT
)
)
searchPanel.add(
new JLabel(
"SEARCH:"
)
)
searchPanel.add(
search
)
searchPanel.add(
searchButton
)
val left =
new JPanel(
new BorderLayout()
)
left.add(
new JLabel(
"GAME LIBRARY",
SwingConstants.CENTER
),
BorderLayout.NORTH
)
left.add(
searchPanel,
BorderLayout.CENTER
)
left.add(
new JScrollPane(
list
),
BorderLayout.SOUTH
)
val right =
new JPanel(
new BorderLayout()
)
right.add(
new JLabel(
"GAME INFORMATION",
SwingConstants.CENTER
),
BorderLayout.NORTH
)
right.add(
new JScrollPane(
info
),
BorderLayout.CENTER
)
val split =
new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT,
left,
right
)
split.setDividerLocation(
380
)
list.addListSelectionListener(
new javax.swing.event.ListSelectionListener {
override def valueChanged(
e: javax.swing.event.ListSelectionEvent
): Unit = {
if (
!e.getValueIsAdjusting
) {
val selected =
list.getSelectedValue
if (
selected != null
) {
var j: Int =
0
while (
j < gameDatabase.length
) {
if (
gameDatabase(j).name ==
selected
) {
info.setText(
"GAME\n\n" +
gameDatabase(j).name +
"\n\nMODE: " +
gameDatabase(j).mode +
"\n\n" +
gameDatabase(j).description
)
}
j +=
1
}
}
}
}
}
)
searchButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val q =
search.getText
.trim
.toLowerCase
model.clear()
var j: Int =
0
while (
j < gameDatabase.length
) {
if (
q.isEmpty ||
gameDatabase(j).name
.toLowerCase
.indexOf(q) >= 0
) {
model.addElement(
gameDatabase(j).name
)
}
j +=
1
}
}
}
)
playButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val selected =
list.getSelectedValue
if (
selected != null
) {
launchGame(
selected
)
} else {
JOptionPane.showMessageDialog(
frame,
"SELECT A GAME FIRST"
)
}
}
}
)
list.addMouseListener(
new MouseAdapter {
override def mouseClicked(
e: MouseEvent
): Unit = {
if (
e.getClickCount == 2
) {
val selected =
list.getSelectedValue
if (
selected != null
) {
launchGame(
selected
)
}
}
}
}
)
codeButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val selected =
list.getSelectedValue
if (
selected != null
) {
generatedAnimationCode =
"// GAME SOURCE: " +
selected +
"\n\n" +
"cleari()\n" +
"setAnimationDelay(5)\n\n" +
"// Launch from ULTRA LEGEND GAME STUDIO."
val codeFrame =
new JFrame(
"GAME CODE"
)
codeFrame.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE
)
codeFrame.setSize(
820,
580
)
codeFrame.setLocationRelativeTo(
frame
)
val area =
new JTextArea(
generatedAnimationCode
)
area.setFont(
new Font(
"Monospaced",
Font.PLAIN,
13
)
)
val copyGame =
button(
"COPY"
)
copyGame.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
copyText(
area.getText
)
}
}
)
codeFrame.add(
new JScrollPane(
area
),
BorderLayout.CENTER
)
codeFrame.add(
copyGame,
BorderLayout.SOUTH
)
codeFrame.setVisible(
true
)
} else {
JOptionPane.showMessageDialog(
frame,
"SELECT A GAME FIRST"
)
}
}
}
)
closeButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
frame.dispose()
}
}
)
val bottom =
new JPanel(
new FlowLayout(
FlowLayout.CENTER
)
)
bottom.add(
playButton
)
bottom.add(
codeButton
)
bottom.add(
closeButton
)
frame.setLayout(
new BorderLayout()
)
frame.add(
split,
BorderLayout.CENTER
)
frame.add(
bottom,
BorderLayout.SOUTH
)
frame.setVisible(
true
)
}
// ============================================================
// MAIN CANVAS
// ============================================================
canvas =
new JPanel {
setFocusable(true)
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
)
g.setColor(
Color.WHITE
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
if (importedImage != null) {
val ix = (getWidth - importedImage.getWidth) / 2
val iy = (getHeight - importedImage.getHeight) / 2
g.drawImage(importedImage, ix, iy, null)
}
if (showGrid) {
g.setColor(
new Color(
0,
0,
0,
20
)
)
var x: Int =
0
while (
x <= CANVAS_W
) {
g.drawLine(
x,
0,
x,
CANVAS_H
)
x +=
50
}
var y: Int =
0
while (
y <= CANVAS_H
) {
g.drawLine(
0,
y,
CANVAS_W,
y
)
y +=
50
}
}
var i: Int =
0
while (
i < drawItems.length
) {
val item =
drawItems(i)
if (
item.visible &&
item.points.length >= 2
) {
if (
item.filled &&
item.points.length >= 3
) {
val path =
new Path2D.Double()
path.moveTo(
item.points.head.x,
item.points.head.y
)
var j: Int =
1
while (
j < item.points.length
) {
path.lineTo(
item.points(j).x,
item.points(j).y
)
j +=
1
}
path.closePath()
g.setColor(
item.fill
)
g.fill(
path
)
}
g.setColor(
item.stroke
)
g.setStroke(
new BasicStroke(
brushWidth,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
)
)
var j: Int =
1
while (
j < item.points.length
) {
g.drawLine(
item.points(j - 1).x,
item.points(j - 1).y,
item.points(j).x,
item.points(j).y
)
j +=
1
}
}
i +=
1
}
if (
tempPoints.length >= 2
) {
g.setColor(
strokeColor
)
g.setStroke(
new BasicStroke(
brushWidth,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
)
)
var j: Int =
1
while (
j < tempPoints.length
) {
g.drawLine(
tempPoints(j - 1).x,
tempPoints(j - 1).y,
tempPoints(j).x,
tempPoints(j).y
)
j +=
1
}
}
if (
selectedIndex >= 0 &&
selectedIndex < drawItems.length &&
activeTool == "SELECT"
) {
val r =
boundsOf(
drawItems(
selectedIndex
)
)
val box =
new java.awt.Rectangle(
r.x - 8,
r.y - 8,
r.width + 16,
r.height + 16
)
g.setColor(
new Color(
0,
105,
240
)
)
g.setStroke(
new BasicStroke(
2
)
)
g.drawRect(
box.x,
box.y,
box.width,
box.height
)
val hs =
handles(
box
)
var h: Int =
0
while (
h < hs.length
) {
val hp =
hs(h)._2
g.setColor(
Color.WHITE
)
g.fillRect(
hp.x - 5,
hp.y - 5,
10,
10
)
g.setColor(
new Color(
0,
105,
240
)
)
g.drawRect(
hp.x - 5,
hp.y - 5,
10,
10
)
h +=
1
}
}
}
}
canvas.setFocusable(
true
)
// ============================================================
// CANVAS MOUSE INPUT
// ============================================================
canvas.addMouseListener(
new MouseAdapter {
override def mousePressed(
e: MouseEvent
): Unit = {
val p =
canvasPoint(e)
if (
activeTool == "SELECT"
) {
val hit =
findObject(p)
if (
selectedIndex >= 0 &&
selectedIndex < drawItems.length
) {
val handle =
resizeHandle(
drawItems(
selectedIndex
),
p
)
if (
handle != ""
) {
if (
!drawItems(
selectedIndex
).locked
) {
saveHistory()
resizingNow =
true
movingNow =
false
resizeHandle =
handle
originalPoints =
drawItems(
selectedIndex
).points
lastMousePoint =
p
}
} else if (
hit == selectedIndex
) {
if (
!drawItems(
selectedIndex
).locked
) {
saveHistory()
movingNow =
true
resizingNow =
false
lastMousePoint =
p
}
} else {
selectedIndex =
hit
}
} else {
selectedIndex =
hit
}
repaintCanvas()
} else if (
activeTool == "ERASER"
) {
val hit =
findObject(p)
if (
hit >= 0 &&
!drawItems(hit).locked
) {
saveHistory()
drawItems.remove(
hit
)
selectedIndex =
-1
repaintCanvas()
setStatus(
"ERASED"
)
}
} else if (
activeTool == "BUCKET"
) {
val hit =
findObject(p)
if (
hit >= 0
) {
selectedIndex =
hit
fillSelected()
} else {
setStatus(
"CLICK A CLOSED OBJECT"
)
}
} else {
saveHistory()
tempPoints.clear()
tempPoints +=
p
drawingNow =
true
selectedIndex =
-1
setStatus(
"DRAWING..."
)
}
}
override def mouseReleased(
e: MouseEvent
): Unit = {
if (
activeTool == "SELECT"
) {
movingNow =
false
resizingNow =
false
repaintCanvas()
} else if (
drawingNow
) {
tempPoints +=
canvasPoint(e)
if (
tempPoints.length >= 2
) {
drawItems +=
DrawItem(
tempPoints.toVector,
strokeColor,
fillColor,
closed(
tempPoints.toVector
),
"FREEHAND",
true,
false
)
selectedIndex =
drawItems.length - 1
}
tempPoints.clear()
drawingNow =
false
repaintCanvas()
setStatus(
"DRAWING RECORDED"
)
}
}
}
)
// ============================================================
// CANVAS DRAG INPUT
// ============================================================
canvas.addMouseMotionListener(
new MouseMotionAdapter {
override def mouseDragged(
e: MouseEvent
): Unit = {
val p =
canvasPoint(e)
if (
activeTool == "SELECT"
) {
if (
movingNow &&
selectedIndex >= 0
) {
moveSelected(
p.x -
lastMousePoint.x,
p.y -
lastMousePoint.y
)
lastMousePoint =
p
repaintCanvas()
} else if (
resizingNow &&
selectedIndex >= 0
) {
resizeSelected(
p
)
repaintCanvas()
}
} else if (
drawingNow
) {
if (
tempPoints.isEmpty ||
distance(
tempPoints.last,
p
) >= 2.0
) {
tempPoints +=
p
repaintCanvas()
}
}
}
}
)
canvas.setTransferHandler(imageDropHandler())
// Ctrl+V in Paint pastes an image directly from the system clipboard.
canvas.addKeyListener(
new KeyAdapter {
override def keyPressed(e: KeyEvent): Unit = {
if (e.isControlDown && e.getKeyCode == KeyEvent.VK_V) {
pasteImageFromClipboard()
}
}
}
)
// ============================================================
// TOOLBAR BUTTONS
// ============================================================
val pencilBtn =
button(
"PENCIL"
)
val selectBtn =
button(
"SELECT"
)
val eraserBtn =
button(
"ERASER"
)
val bucketBtn =
button(
"BUCKET"
)
val undoBtn =
button(
"UNDO"
)
val redoBtn =
button(
"REDO"
)
val clearBtn =
button(
"CLEAR"
)
val drawCodeBtn =
button(
"DRAW CODE"
)
val animationBtn =
button(
"ANIMATION"
)
val animationStudioBtn =
button(
"ANIMATION DRAW"
)
val copyBtn =
button(
"COPY"
)
val saveBtn =
button(
"SAVE"
)
val gamesBtn =
button(
"GAMES"
)
val gridBtn =
button(
"GRID"
)
val snapBtn =
button(
"SNAP"
)
val strokeBtn =
button(
"STROKE"
)
val fillBtn =
button(
"FILL"
)
val importImageBtn =
button(
"IMPORT IMAGE"
)
val pasteImageBtn =
button(
"PASTE IMAGE"
)
val restoreMediaCodeBtn =
button(
"RESTORE MEDIA CODE"
)
// ============================================================
// TOOLBAR
// ============================================================
val toolbar =
new JPanel(
new FlowLayout(
FlowLayout.LEFT,
3,
3
)
)
toolbar.add(
pencilBtn
)
toolbar.add(
selectBtn
)
toolbar.add(
eraserBtn
)
toolbar.add(
bucketBtn
)
toolbar.add(
undoBtn
)
toolbar.add(
redoBtn
)
toolbar.add(
clearBtn
)
toolbar.add(
drawCodeBtn
)
toolbar.add(
animationBtn
)
toolbar.add(
copyBtn
)
toolbar.add(
saveBtn
)
toolbar.add(
gamesBtn
)
toolbar.add(
gridBtn
)
toolbar.add(
snapBtn
)
toolbar.add(
strokeBtn
)
toolbar.add(
fillBtn
)
toolbar.add(
importImageBtn
)
toolbar.add(
pasteImageBtn
)
toolbar.add(
restoreMediaCodeBtn
)
// ============================================================
// OBJECT / SHAPE LIST UI
// ============================================================
objectList =
new JList[String](
objectModel
)
objectList.setFont(
new Font(
"Arial",
Font.BOLD,
13
)
)
shapeList =
new JList[String](
shapeModel
)
shapeList.setFont(
new Font(
"Arial",
Font.BOLD,
13
)
)
objectSearch =
new JTextField(
12
)
val objectSearchBtn =
button(
"SEARCH"
)
val objectSearchPanel =
new JPanel(
new FlowLayout(
FlowLayout.LEFT
)
)
objectSearchPanel.add(
objectSearch
)
objectSearchPanel.add(
objectSearchBtn
)
val objectPanel =
new JPanel(
new BorderLayout()
)
objectPanel.add(
new JLabel(
"OBJECT LIBRARY",
SwingConstants.CENTER
),
BorderLayout.NORTH
)
objectPanel.add(
objectSearchPanel,
BorderLayout.CENTER
)
objectPanel.add(
new JScrollPane(
objectList
),
BorderLayout.SOUTH
)
val shapePanel =
new JPanel(
new BorderLayout()
)
shapePanel.add(
new JLabel(
"SHAPE LIBRARY",
SwingConstants.CENTER
),
BorderLayout.NORTH
)
shapePanel.add(
new JScrollPane(
shapeList
),
BorderLayout.CENTER
)
val libraryTabs =
new JTabbedPane()
libraryTabs.addTab(
"OBJECTS",
objectPanel
)
libraryTabs.addTab(
"SHAPES",
shapePanel
)
// ============================================================
// SEARCH
// ============================================================
objectSearchBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val q =
objectSearch
.getText
.trim
.toLowerCase
objectModel.clear()
var i: Int =
0
while (
i < objects.length
) {
if (
q.isEmpty ||
objects(i)
.toLowerCase
.indexOf(q) >= 0
) {
objectModel.addElement(
objects(i)
)
}
i +=
1
}
}
}
)
// ============================================================
// DOUBLE CLICK OBJECT
// ============================================================
objectList.addMouseListener(
new MouseAdapter {
override def mouseClicked(
e: MouseEvent
): Unit = {
if (
e.getClickCount == 2
) {
val selected =
objectList.getSelectedValue
if (
selected != null
) {
addObject(
selected
)
}
}
}
}
)
// ============================================================
// DOUBLE CLICK SHAPE
// ============================================================
shapeList.addMouseListener(
new MouseAdapter {
override def mouseClicked(
e: MouseEvent
): Unit = {
if (
e.getClickCount == 2
) {
val selected =
shapeList.getSelectedValue
if (
selected != null
) {
addShape(
selected
)
}
}
}
}
)
// ============================================================
// CODE AREA
// ============================================================
codeArea =
new JTextArea()
codeArea.setEditable(
false
)
codeArea.setFont(
new Font(
"Monospaced",
Font.PLAIN,
13
)
)
codeArea.setText(
"// DRAW SOMETHING\n" +
"// THEN PRESS DRAW CODE"
)
// ============================================================
// PANELS
// ============================================================
val canvasPanel =
new JPanel(
new BorderLayout()
)
canvasPanel.add(
new JLabel(
"DRAWING CANVAS",
SwingConstants.CENTER
),
BorderLayout.NORTH
)
canvasPanel.add(
canvas,
BorderLayout.CENTER
)
val codePanel =
new JPanel(
new BorderLayout()
)
val codeHeader =
new JPanel(
new BorderLayout()
)
codeHeader.add(
new JLabel(
"GENERATED KOJO / SCALA CODE",
SwingConstants.CENTER
),
BorderLayout.CENTER
)
val restoreMediaCodeB =
button("RESTORE IMPORTED MEDIA CODE")
restoreMediaCodeB.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
restoreImportedMediaCode()
}
}
)
codeHeader.add(
restoreMediaCodeB,
BorderLayout.SOUTH
)
codePanel.add(
codeHeader,
BorderLayout.NORTH
)
codePanel.add(
new JScrollPane(
codeArea
),
BorderLayout.CENTER
)
val canvasCodeSplit =
new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT,
canvasPanel,
codePanel
)
canvasCodeSplit.setDividerLocation(
660
)
val mainSplit =
new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT,
libraryTabs,
canvasCodeSplit
)
mainSplit.setDividerLocation(
285
)
// ============================================================
// STATUS
// ============================================================
statusLabel =
new JLabel(
"ULTRA LEGEND X9 READY"
)
statusLabel.setFont(
new Font(
"Arial",
Font.BOLD,
13
)
)
// ============================================================
// MAIN FRAME
// ============================================================
mainFrame =
new JFrame(
"ULTRA LEGEND DRAW + GAME STUDIO X9"
)
mainFrame.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE
)
mainFrame.setSize(
1650,
900
)
mainFrame.setMinimumSize(
new Dimension(
1100,
650
)
)
mainFrame.setLocationRelativeTo(
null
)
mainFrame.setLayout(
new BorderLayout(
4,
4
)
)
mainFrame.add(
toolbar,
BorderLayout.NORTH
)
mainFrame.add(
mainSplit,
BorderLayout.CENTER
)
mainFrame.add(
statusLabel,
BorderLayout.SOUTH
)
// ============================================================
// TOOL EVENTS
// ============================================================
pencilBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
activeTool =
"PENCIL"
selectedIndex =
-1
canvas.setCursor(
pencilCursor
)
setStatus(
"PENCIL MODE"
)
canvas.requestFocusInWindow()
}
}
)
selectBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
activeTool =
"SELECT"
canvas.setCursor(
Cursor.getPredefinedCursor(
Cursor.DEFAULT_CURSOR
)
)
setStatus(
"SELECT / MOVE / RESIZE"
)
repaintCanvas()
canvas.requestFocusInWindow()
}
}
)
eraserBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
activeTool =
"ERASER"
selectedIndex =
-1
canvas.setCursor(
pencilCursor
)
setStatus(
"ERASER MODE"
)
canvas.requestFocusInWindow()
}
}
)
bucketBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
activeTool =
"BUCKET"
canvas.setCursor(
Cursor.getPredefinedCursor(
Cursor.HAND_CURSOR
)
)
setStatus(
"BUCKET MODE"
)
canvas.requestFocusInWindow()
}
}
)
undoBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
undo()
}
}
)
redoBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
redo()
}
}
)
clearBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
clearAll()
}
}
)
importImageBtn.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
chooseAndImportImage(mainFrame)
}
}
)
pasteImageBtn.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
pasteImageFromClipboard()
}
}
)
restoreMediaCodeBtn.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
restoreImportedMediaCode()
}
}
)
drawCodeBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
generateCode()
}
}
)
animationBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
// Open the independent Animation Studio window.
// The regular ANIMATION button is intentionally a window launcher.
openFrameAnimationStudio()
}
}
)
animationStudioBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
openFrameAnimationStudio()
}
}
)
copyBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
copyText(
codeArea.getText
)
}
}
)
saveBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
saveText(
codeArea.getText
)
}
}
)
gamesBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
openGameStudio()
}
}
)
gridBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
showGrid =
!showGrid
repaintCanvas()
if (showGrid)
setStatus("GRID ON")
else
setStatus("GRID OFF")
}
}
)
snapBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
snapToGrid =
!snapToGrid
if (snapToGrid)
setStatus("SNAP ON")
else
setStatus("SNAP OFF")
}
}
)
strokeBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val selected =
JColorChooser.showDialog(
mainFrame,
"CHOOSE STROKE COLOR",
strokeColor
)
if (
selected != null
) {
strokeColor =
selected
setStatus(
"STROKE COLOR CHANGED"
)
}
}
}
)
fillBtn.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val selected =
JColorChooser.showDialog(
mainFrame,
"CHOOSE FILL COLOR",
fillColor
)
if (
selected != null
) {
fillColor =
selected
if (
selectedIndex >= 0
) {
fillSelected()
} else {
setStatus(
"FILL COLOR CHANGED"
)
}
}
}
}
)
// ============================================================
// KEYBOARD SHORTCUTS
// ============================================================
canvas.addKeyListener(
new KeyAdapter {
override def keyPressed(
e: KeyEvent
): Unit = {
val k: Int =
e.getKeyCode
if (
e.isControlDown &&
k == KeyEvent.VK_Z
) {
undo()
} else if (
e.isControlDown &&
k == KeyEvent.VK_Y
) {
redo()
} else if (
e.isControlDown &&
k == KeyEvent.VK_G
) {
generateCode()
} else if (
e.isControlDown &&
k == KeyEvent.VK_S
) {
saveText(
codeArea.getText
)
} else if (
e.isControlDown &&
k == KeyEvent.VK_C
) {
copyText(
codeArea.getText
)
} else if (
e.isControlDown &&
k == KeyEvent.VK_D
) {
openGameStudio()
} else if (
k == KeyEvent.VK_DELETE
) {
deleteSelected()
} else if (
k == KeyEvent.VK_P
) {
activeTool =
"PENCIL"
setStatus(
"PENCIL"
)
} else if (
k == KeyEvent.VK_V
) {
activeTool =
"SELECT"
setStatus(
"SELECT"
)
} else if (
k == KeyEvent.VK_B
) {
activeTool =
"BUCKET"
setStatus(
"BUCKET"
)
} else if (
k == KeyEvent.VK_E
) {
activeTool =
"ERASER"
setStatus(
"ERASER"
)
} else if (
k == KeyEvent.VK_F1
) {
JOptionPane.showMessageDialog(
mainFrame,
"ULTRA LEGEND X9\n\n" +
"P = Pencil\n" +
"V = Select\n" +
"B = Bucket\n" +
"E = Eraser\n" +
"DELETE = Delete\n\n" +
"CTRL+Z = Undo\n" +
"CTRL+Y = Redo\n" +
"CTRL+G = Draw Code\n" +
"CTRL+C = Copy\n" +
"CTRL+S = Save\n" +
"CTRL+D = Game Studio\n\n" +
"SELECT + DRAG = MOVE\n" +
"SELECT + HANDLE = RESIZE",
"SHORTCUTS",
JOptionPane.INFORMATION_MESSAGE
)
}
}
}
)
// ============================================================
// ============================================================
// SMOOTH PENCIL CURSOR + SIMPLE SOUND SUPPORT
// ============================================================
var selectedSoundFile: String = ""
var soundEnabled: Boolean = true
def makePencilCursor(): Cursor = {
try {
val img =
new BufferedImage(
32,
32,
BufferedImage.TYPE_INT_ARGB
)
val cg =
img.createGraphics()
cg.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
)
cg.setColor(
new Color(246, 205, 128)
)
val body =
new java.awt.Polygon()
body.addPoint(6, 4)
body.addPoint(24, 22)
body.addPoint(19, 27)
body.addPoint(1, 9)
cg.fillPolygon(body)
cg.setColor(
new Color(70, 45, 30)
)
cg.fillPolygon(
Array(1, 9, 6, 4),
Array(9, 28, 4, 4),
4
)
cg.setColor(
new Color(220, 65, 70)
)
cg.fillRect(
20,
18,
7,
7
)
cg.dispose()
val toolkit =
java.awt.Toolkit.getDefaultToolkit
toolkit.createCustomCursor(
img,
new Point(2, 28),
"pencil"
)
} catch {
case _: Exception =>
pencilCursor
}
}
val pencilCursor =
makePencilCursor()
def playStudioBeep(): Unit = {
if (soundEnabled) {
try {
Toolkit.getDefaultToolkit.beep()
} catch {
case _: Exception =>
}
}
}
// ============================================================
// ULTRA INDEPENDENT FRAME ANIMATION STUDIO
// ============================================================
val animationCommandWords = Array(
"NONE",
"MOVE LEFT",
"MOVE RIGHT",
"MOVE UP",
"MOVE DOWN",
"JUMP",
"BOUNCE",
"WALK",
"RUN",
"FLOAT",
"FALL",
"RISE",
"SHAKE",
"WIGGLE",
"WAVE",
"DANCE",
"SPIN",
"ROTATE LEFT",
"ROTATE RIGHT",
"FLIP HORIZONTAL",
"FLIP VERTICAL",
"SCALE UP",
"SCALE DOWN",
"PULSE",
"SWING",
"ORBIT",
"ORBIT REVERSE",
"CIRCLE MOVE",
"SPIRAL MOVE",
"FIGURE EIGHT",
"ZIGZAG",
"JELLY",
"HEARTBEAT",
"TWINKLE",
"SPARKLE",
"VIBRATE",
"TREMBLE",
"TILT LEFT",
"TILT RIGHT",
"ROCK",
"SPRING",
"POP",
"SLIDE LEFT",
"SLIDE RIGHT",
"SLIDE UP",
"SLIDE DOWN",
"FLY",
"SOAR",
"LAND",
"TAKE OFF",
"CAR DRIVE",
"CAR REVERSE",
"WHEEL SPIN",
"BALL ROLL",
"BALL BOUNCE",
"CLOUD DRIFT",
"RAIN FALL",
"SNOW FALL",
"FIRE FLICKER",
"WATER WAVE",
"TREE SWAY",
"LEAF FALL",
"BIRD FLY",
"BUTTERFLY FLUTTER",
"FISH SWIM",
"CLOCK TICK",
"CANDLE FLICKER",
"LIGHT FLASH",
"MAGIC APPEAR",
"MAGIC DISAPPEAR",
"TELEPORT",
"PORTAL",
"LASER",
"EXPLODE",
"PARTICLE BURST",
"RING PULSE",
"STAR SPIN",
"MOON ORBIT",
"PLANET ORBIT",
"SUN PULSE",
"ELECTRIC",
"NEON PULSE",
"GLITCH",
"CYBER MOVE",
"FUTURE DRIVE",
"TIME WARP",
"TIME REWIND",
"SPEED UP",
"SLOW MOTION",
"EASE IN",
"EASE OUT",
"EASE IN OUT",
"SPRING IN",
"SPRING OUT",
"ELASTIC",
"RANDOM SHAKE",
"RANDOM MOVE",
"REVERSE",
"PING PONG",
"POSE",
"STEP",
"CHASE",
"PATROL",
"FOLLOW"
)
def animationClone(source: ArrayBuffer[DrawItem]): ArrayBuffer[DrawItem] = {
cloneItems(source)
}
def animationSnapshot(): Unit = {
ensureAnimationImageSlots()
if (animationFrames.isEmpty) {
animationFrames += animationClone(drawItems)
animationImageFrames += null
} else if (
animationCurrentFrame >= 0 &&
animationCurrentFrame < animationFrames.length
) {
animationFrames(animationCurrentFrame) = animationClone(drawItems)
}
}
def animationLoad(index: Int): Unit = {
ensureAnimationImageSlots()
if (animationFrames.nonEmpty) {
val safe =
math.max(
0,
math.min(
animationFrames.length - 1,
index
)
)
drawItems.clear()
drawItems ++= animationClone(animationFrames(safe))
animationCurrentFrame = safe
selectedIndex = -1
repaintCanvas()
}
}
def animationNextFrame(): Unit = {
animationSnapshot()
animationFrames += ArrayBuffer[DrawItem]()
animationImageFrames += null
animationCurrentFrame = animationFrames.length - 1
drawItems.clear()
selectedIndex = -1
}
def animationCopy(): Unit = {
animationSnapshot()
animationClipboard.clear()
if (
selectedIndex >= 0 &&
selectedIndex < drawItems.length
) {
animationClipboard +=
animationClone(
ArrayBuffer(drawItems(selectedIndex))
).head
} else {
animationClipboard ++=
animationClone(drawItems)
}
}
def animationPaste(): Unit = {
if (animationClipboard.nonEmpty) {
val pasted = animationClone(animationClipboard)
val shifted =
pasted.map { item =>
item.copy(
points =
item.points.map { p =>
new Point(
p.x + 25,
p.y + 15
)
}
)
}
drawItems ++= shifted
if (drawItems.nonEmpty) {
selectedIndex = drawItems.length - 1
}
}
}
def animationTransform(
source: ArrayBuffer[DrawItem],
command: String,
step: Int
): ArrayBuffer[DrawItem] = {
val out =
animationClone(source)
var minX = 0.0
var maxX = 0.0
var minY = 0.0
var maxY = 0.0
var pointCount = 0
var i = 0
while (i < out.length) {
var j = 0
while (j < out(i).points.length) {
val p = out(i).points(j)
if (pointCount == 0) {
minX = p.x.toDouble
maxX = p.x.toDouble
minY = p.y.toDouble
maxY = p.y.toDouble
} else {
minX = math.min(minX, p.x.toDouble)
maxX = math.max(maxX, p.x.toDouble)
minY = math.min(minY, p.y.toDouble)
maxY = math.max(maxY, p.y.toDouble)
}
pointCount += 1
j += 1
}
i += 1
}
if (pointCount == 0) {
return out
}
val cx =
(minX + maxX) / 2.0
val cy =
(minY + maxY) / 2.0
val t =
step.toDouble
val phase =
t / 12.0
i = 0
while (i < out.length) {
val item =
out(i)
val pts =
item.points.map { p =>
var nx = p.x.toDouble
var ny = p.y.toDouble
if (
command == "MOVE LEFT" ||
command == "SLIDE LEFT"
) {
nx -= t * 5.0
} else if (
command == "MOVE RIGHT" ||
command == "SLIDE RIGHT"
) {
nx += t * 5.0
} else if (
command == "MOVE UP" ||
command == "RISE" ||
command == "SLIDE UP"
) {
ny -= t * 4.0
} else if (
command == "MOVE DOWN" ||
command == "FALL" ||
command == "SLIDE DOWN"
) {
ny += t * 4.0
} else if (
command == "JUMP" ||
command == "BOUNCE" ||
command == "SPRING"
) {
ny -=
math.sin(phase * math.Pi) * 60.0
} else if (
command == "WALK" ||
command == "STEP"
) {
nx +=
math.sin(t * 0.55) * 10.0
ny -=
math.abs(math.sin(t * 0.55)) * 6.0
} else if (command == "RUN") {
nx += t * 7.0
ny += math.sin(t * 0.9) * 7.0
} else if (
command == "FLOAT" ||
command == "SOAR"
) {
ny -= math.sin(t * 0.35) * 18.0
} else if (
command == "SCALE UP" ||
command == "PULSE" ||
command == "HEARTBEAT"
) {
val k =
1.0 + math.sin(phase * math.Pi) * 0.12
nx =
cx + (nx - cx) * k
ny =
cy + (ny - cy) * k
} else if (command == "SCALE DOWN") {
val k =
1.0 - math.sin(phase * math.Pi) * 0.10
nx =
cx + (nx - cx) * k
ny =
cy + (ny - cy) * k
} else if (
command == "ROTATE LEFT" ||
command == "SPIN" ||
command == "STAR SPIN"
) {
val a =
-t * 7.0 * math.Pi / 180.0
val dx =
nx - cx
val dy =
ny - cy
val rotatedX =
dx * math.cos(a) - dy * math.sin(a)
val rotatedY =
dx * math.sin(a) + dy * math.cos(a)
nx =
cx + rotatedX
ny =
cy + rotatedY
} else if (
command == "ROTATE RIGHT" ||
command == "TILT RIGHT"
) {
val a =
t * 7.0 * math.Pi / 180.0
val dx =
nx - cx
val dy =
ny - cy
val rotatedX =
dx * math.cos(a) - dy * math.sin(a)
val rotatedY =
dx * math.sin(a) + dy * math.cos(a)
nx =
cx + rotatedX
ny =
cy + rotatedY
} else if (
command == "FLIP HORIZONTAL"
) {
nx =
cx - (nx - cx)
} else if (
command == "FLIP VERTICAL"
) {
ny =
cy - (ny - cy)
} else if (
command == "WAVE" ||
command == "WIGGLE" ||
command == "TREE SWAY" ||
command == "WATER WAVE"
) {
ny +=
math.sin(
p.x.toDouble * 0.05 +
t * 0.55
) * 12.0
} else if (
command == "SHAKE" ||
command == "VIBRATE" ||
command == "TREMBLE" ||
command == "RANDOM SHAKE"
) {
nx +=
math.sin(t * 2.4) * 11.0
ny +=
math.cos(t * 2.0) * 8.0
} else if (
command == "SWING" ||
command == "ROCK" ||
command == "TILT LEFT"
) {
val a =
math.sin(phase * math.Pi) *
10.0 * math.Pi / 180.0
val dx =
nx - cx
val dy =
ny - cy
val rotatedX =
dx * math.cos(a) - dy * math.sin(a)
val rotatedY =
dx * math.sin(a) + dy * math.cos(a)
nx =
cx + rotatedX
ny =
cy + rotatedY
} else if (
command == "ORBIT" ||
command == "ORBIT REVERSE" ||
command == "CIRCLE MOVE" ||
command == "MOON ORBIT" ||
command == "PLANET ORBIT"
) {
var orbitSign = 1.0
if (command == "ORBIT REVERSE") {
orbitSign = -1.0
}
nx +=
math.cos(t * 0.35 * orbitSign) * 16.0
ny +=
math.sin(t * 0.35 * orbitSign) * 10.0
} else if (
command == "SPIRAL MOVE"
) {
val radius =
math.min(60.0, t * 1.6)
nx +=
math.cos(t * 0.45) * radius
ny +=
math.sin(t * 0.45) * radius
} else if (
command == "FIGURE EIGHT"
) {
nx +=
math.sin(t * 0.35) * 20.0
ny +=
math.sin(t * 0.70) * 12.0
} else if (
command == "ZIGZAG"
) {
var zig = -14.0
if (step % 2 == 0) {
zig = 14.0
}
nx += zig
ny -= 4.0
} else if (
command == "TWINKLE" ||
command == "SPARKLE"
) {
val s =
math.sin(t * 0.8)
nx =
cx + (nx - cx) * (1.0 + s * 0.05)
ny =
cy + (ny - cy) * (1.0 + s * 0.05)
} else if (
command == "POP" ||
command == "SPRING IN"
) {
val k =
0.70 +
math.sin(phase * math.Pi) * 0.35
nx =
cx + (nx - cx) * k
ny =
cy + (ny - cy) * k
} else if (
command == "SPRING OUT" ||
command == "ELASTIC"
) {
val k =
1.0 +
math.sin(phase * math.Pi * 2.0) * 0.16
nx =
cx + (nx - cx) * k
ny =
cy + (ny - cy) * k
} else if (
command == "JELLY"
) {
nx +=
math.sin(t * 0.8 + p.y * 0.03) * 8.0
ny +=
math.sin(t * 1.0 + p.x * 0.03) * 6.0
} else if (
command == "VIBRATE"
) {
nx += math.sin(t * 4.0) * 5.0
ny += math.cos(t * 4.0) * 5.0
} else if (
command == "DANCE" ||
command == "CYBER MOVE"
) {
nx +=
math.sin(t * 0.60) * 14.0
ny -=
math.abs(math.sin(t * 0.60)) * 10.0
} else if (
command == "FLY" ||
command == "TAKE OFF"
) {
nx += t * 3.0
ny -= t * 2.0
} else if (
command == "LAND"
) {
ny += t * 2.0
} else if (
command == "CAR DRIVE" ||
command == "FUTURE DRIVE"
) {
nx += t * 8.0
ny += math.sin(t * 0.5) * 2.0
} else if (
command == "CAR REVERSE"
) {
nx -= t * 6.0
} else if (
command == "WHEEL SPIN" ||
command == "BALL ROLL"
) {
nx += t * 5.0
ny += math.sin(t * 0.8) * 2.0
} else if (
command == "BALL BOUNCE"
) {
ny -=
math.abs(math.sin(t * 0.6)) * 45.0
} else if (
command == "CLOUD DRIFT"
) {
nx += t * 3.0
ny += math.sin(t * 0.3) * 5.0
} else if (
command == "RAIN FALL" ||
command == "SNOW FALL" ||
command == "LEAF FALL"
) {
ny += t * 4.0
nx += math.sin(t * 0.4) * 10.0
} else if (
command == "BIRD FLY" ||
command == "BUTTERFLY FLUTTER"
) {
nx += t * 4.0
ny += math.sin(t * 0.7) * 20.0
} else if (
command == "FISH SWIM"
) {
nx += t * 3.0
ny += math.sin(t * 0.55) * 12.0
} else if (
command == "CLOCK TICK" ||
command == "CANDLE FLICKER"
) {
val a =
math.sin(t * 0.9) * 3.0 * math.Pi / 180.0
val dx =
nx - cx
val dy =
ny - cy
nx =
cx + dx * math.cos(a) - dy * math.sin(a)
ny =
cy + dx * math.sin(a) + dy * math.cos(a)
} else if (
command == "PULSE" ||
command == "RING PULSE" ||
command == "SUN PULSE" ||
command == "NEON PULSE"
) {
val k =
1.0 + math.sin(t * 0.7) * 0.08
nx =
cx + (nx - cx) * k
ny =
cy + (ny - cy) * k
} else if (
command == "EASE IN" ||
command == "EASE IN OUT"
) {
val e =
(t / 12.0) * (t / 12.0)
nx += e * 20.0
} else if (
command == "EASE OUT"
) {
val q =
t / 12.0
val e =
1.0 - (1.0 - q) * (1.0 - q)
nx += e * 20.0
} else if (
command == "SPEED UP"
) {
nx += t * t * 0.45
} else if (
command == "SLOW MOTION"
) {
nx += t * 2.0
} else if (
command == "RANDOM MOVE"
) {
nx +=
math.sin(t * 1.7 + p.x * 0.01) * 8.0
ny +=
math.cos(t * 1.3 + p.y * 0.01) * 8.0
} else if (
command == "PING PONG"
) {
var direction = 1.0
if (step > 6) {
direction = -1.0
}
nx +=
direction * math.min(30.0, math.abs(t - 6.0) * 5.0)
} else if (
command == "REVERSE"
) {
nx -= t * 4.0
} else if (
command == "TELEPORT"
) {
if (step >= 6) {
nx += 180.0
}
} else if (
command == "MAGIC APPEAR" ||
command == "MAGIC DISAPPEAR" ||
command == "BLINK"
) {
nx +=
math.sin(t * 1.4) * 2.0
} else if (
command == "TIME WARP"
) {
val q =
math.sin(t * 0.5)
nx += q * t
ny -= q * 0.5 * t
} else if (
command == "TIME REWIND"
) {
nx -= t * 3.0
} else if (
command == "POSE"
) {
nx +=
math.sin(t * 0.5) * 2.0
} else if (
command == "CHASE" ||
command == "PATROL" ||
command == "FOLLOW"
) {
nx += t * 3.5
ny += math.sin(t * 0.5) * 7.0
}
new Point(
nx.toInt,
ny.toInt
)
}.toVector
out(i) =
item.copy(
points = pts
)
i += 1
}
out
}
// ============================================================
// DRAW ONE FRAME
// ============================================================
def paintFrameScene(
g: Graphics2D,
source: ArrayBuffer[DrawItem],
ghost: ArrayBuffer[DrawItem],
showGhost: Boolean,
selected: Int
): Unit = {
g.setColor(Color.WHITE)
g.fillRect(
0,
0,
CANVAS_W,
CANVAS_H
)
g.setColor(
new Color(235, 238, 245)
)
var gx = 0
while (gx <= CANVAS_W) {
g.drawLine(
gx,
0,
gx,
CANVAS_H
)
gx += 25
}
var gy = 0
while (gy <= CANVAS_H) {
g.drawLine(
0,
gy,
CANVAS_W,
gy
)
gy += 25
}
def drawItemsWithAlpha(
items: ArrayBuffer[DrawItem],
alpha: Int
): Unit = {
var i = 0
while (i < items.length) {
val item =
items(i)
if (
item.visible &&
item.points.length >= 2
) {
val oldComposite =
g.getComposite
g.setComposite(
java.awt.AlphaComposite.getInstance(
java.awt.AlphaComposite.SRC_OVER,
alpha.toFloat / 255.0f
)
)
if (
item.filled &&
item.points.length >= 3
) {
g.setColor(item.fill)
val xs =
item.points.map(_.x).toArray
val ys =
item.points.map(_.y).toArray
g.fillPolygon(
xs,
ys,
xs.length
)
}
g.setColor(item.stroke)
g.setStroke(
new BasicStroke(
math.max(
1.0f,
brushWidth
),
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
)
)
var j = 1
while (j < item.points.length) {
g.drawLine(
item.points(j - 1).x,
item.points(j - 1).y,
item.points(j).x,
item.points(j).y
)
j += 1
}
g.setComposite(oldComposite)
}
i += 1
}
}
if (showGhost && ghost.nonEmpty) {
drawItemsWithAlpha(
ghost,
55
)
}
drawItemsWithAlpha(
source,
255
)
if (
selected >= 0 &&
selected < source.length
) {
val r =
boundsOf(
source(selected)
)
g.setColor(
new Color(
30,
120,
255,
190
)
)
g.setStroke(
new BasicStroke(
2.0f,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
)
)
g.drawRect(
r.x - 6,
r.y - 6,
r.width + 12,
r.height + 12
)
val handlesNow =
handles(r)
var h = 0
while (h < handlesNow.length) {
val hp = handlesNow(h)._2
g.fillRect(
hp.x - 4,
hp.y - 4,
8,
8
)
h += 1
}
}
}
// ============================================================
// GENERATED FRAME ANIMATION CODE
// ============================================================
def generatedAnimationItemsCode(
items: ArrayBuffer[DrawItem],
indent: String
): String = {
val sb =
new StringBuilder()
var i = 0
while (i < items.length) {
val item =
items(i)
if (
item.visible &&
item.points.length >= 2
) {
sb.append(
indent +
"// " +
item.name.replace(
"\"",
"'"
) +
"\n"
)
sb.append(
indent +
"val pts" +
i +
" = Array(\n"
)
var j = 0
while (j < item.points.length) {
sb.append(
indent +
" new Point(" +
item.points(j).x +
"," +
item.points(j).y +
")"
)
if (
j < item.points.length - 1
) {
sb.append(",")
}
sb.append("\n")
j += 1
}
sb.append(
indent +
")\n"
)
if (
item.filled &&
item.points.length >= 3
) {
sb.append(
indent +
"g.setColor(new Color(" +
item.fill.getRed +
"," +
item.fill.getGreen +
"," +
item.fill.getBlue +
"))\n"
)
sb.append(
indent +
"val fx" +
i +
" = pts" +
i +
".map(_.x).toArray\n"
)
sb.append(
indent +
"val fy" +
i +
" = pts" +
i +
".map(_.y).toArray\n"
)
sb.append(
indent +
"g.fillPolygon(fx" +
i +
", fy" +
i +
", fx" +
i +
".length)\n"
)
}
sb.append(
indent +
"g.setColor(new Color(" +
item.stroke.getRed +
"," +
item.stroke.getGreen +
"," +
item.stroke.getBlue +
"))\n"
)
sb.append(
indent +
"g.setStroke(new BasicStroke(" +
math.max(1.0f, brushWidth) +
"f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND))\n"
)
sb.append(
indent +
"var k" +
i +
" = 1\n"
)
sb.append(
indent +
"while (k" +
i +
" < pts" +
i +
".length) {\n"
)
sb.append(
indent +
" g.drawLine(pts" +
i +
"(k" +
i +
" - 1).x, pts" +
i +
"(k" +
i +
" - 1).y, pts" +
i +
"(k" +
i +
").x, pts" +
i +
"(k" +
i +
").y)\n"
)
sb.append(
indent +
" k" +
i +
" += 1\n"
)
sb.append(
indent +
"}\n"
)
}
i += 1
}
sb.toString
}
def generateFrameAnimationStudioCode(): Unit = {
animationSnapshot()
val sb =
new StringBuilder()
sb.append(
"import java.awt.BasicStroke\n"
)
sb.append(
"import java.awt.Color\n"
)
sb.append(
"import java.awt.Graphics2D\n"
)
sb.append(
"import java.awt.Point\n"
)
sb.append(
"import java.awt.RenderingHints\n"
)
sb.append(
"import java.awt.event.ActionEvent\n"
)
sb.append(
"import javax.swing.JFrame\n"
)
sb.append(
"import javax.swing.JPanel\n"
)
sb.append(
"import javax.swing.Timer\n"
)
sb.append(
"import javax.swing.WindowConstants\n"
)
sb.append(
"import java.io.ByteArrayInputStream\n"
)
sb.append(
"import java.util.Base64\n"
)
sb.append(
"import javax.imageio.ImageIO\n"
)
sb.append(
"import javax.sound.sampled.AudioSystem\n"
)
sb.append(
"import javax.sound.sampled.Clip\n\n"
)
sb.append(
"val W = " + CANVAS_W + "\n"
)
sb.append(
"val H = " + CANVAS_H + "\n"
)
sb.append(
"var frameNo = 0\n"
)
sb.append(
"val voiceOverFile = \"\"\n"
)
sb.append(
"def playVoice(): Unit = {\n"
)
sb.append(
" if (voiceOverFile != \"\") {\n"
)
sb.append(
" try {\n"
)
sb.append(
" val clip = AudioSystem.getClip\n"
)
sb.append(
" val ais = AudioSystem.getAudioInputStream(new java.io.File(voiceOverFile))\n"
)
sb.append(
" clip.open(ais)\n"
)
sb.append(
" clip.start()\n"
)
sb.append(
" } catch {\n"
)
sb.append(
" case _: Exception =>\n"
)
sb.append(
" }\n"
)
sb.append(
" }\n"
)
sb.append(
"}\n\n"
)
sb.append(
"val w = new JFrame(\"GENERATED ANIMATION\")\n"
)
sb.append(
"w.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)\n"
)
sb.append(
"w.setSize(1280, 820)\n"
)
sb.append(
"w.setExtendedState(JFrame.MAXIMIZED_BOTH)\n"
)
sb.append(
"w.setLocationRelativeTo(null)\n\n"
)
sb.append(
"val panel = new JPanel {\n"
)
sb.append(
" override def paintComponent(graphics: java.awt.Graphics): Unit = {\n"
)
sb.append(
" super.paintComponent(graphics)\n"
)
sb.append(
" val g = graphics.asInstanceOf[Graphics2D]\n"
)
sb.append(
" g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)\n"
)
sb.append(
" g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)\n"
)
sb.append(
" val sx = getWidth.toDouble / W.toDouble\n"
)
sb.append(
" val sy = getHeight.toDouble / H.toDouble\n"
)
sb.append(
" val scale = math.max(0.1, math.min(sx, sy))\n"
)
sb.append(
" val drawW = W * scale\n"
)
sb.append(
" val drawH = H * scale\n"
)
sb.append(
" val ox = (getWidth - drawW) / 2.0\n"
)
sb.append(
" val oy = (getHeight - drawH) / 2.0\n"
)
sb.append(
" g.setColor(Color.BLACK)\n"
)
sb.append(
" g.fillRect(0, 0, getWidth, getHeight)\n"
)
sb.append(
" g.translate(ox, oy)\n"
)
sb.append(
" g.scale(scale, scale)\n"
)
sb.append(
" g.setColor(Color.WHITE)\n"
)
sb.append(
" g.fillRect(0, 0, W, H)\n"
)
var f = 0
while (f < animationFrames.length) {
sb.append(
" if (frameNo == " +
f +
") {\n"
)
if (f < animationImageFrames.length && animationImageFrames(f) != null) {
val encodedImage = java.util.Base64.getEncoder.encodeToString(imageToPngBytes(animationImageFrames(f)))
sb.append(" val imageData" + f + " = Array(\n")
var imageChunk = 0
while (imageChunk < encodedImage.length) {
val imageEnd = math.min(imageChunk + 120, encodedImage.length)
sb.append(" \"" + encodedImage.substring(imageChunk, imageEnd) + "\"")
if (imageEnd < encodedImage.length) sb.append(",")
sb.append("\n")
imageChunk = imageEnd
}
sb.append(" )\n")
sb.append(" val img" + f + " = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder.decode(imageData" + f + ".mkString)))\n")
sb.append(" g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)\n")
sb.append(" g.drawImage(img" + f + ", 0, 0, W, H, null)\n")
}
sb.append(
generatedAnimationItemsCode(
animationFrames(f),
" "
)
)
sb.append(
" }\n"
)
f += 1
}
sb.append(
" g.setColor(Color.DARK_GRAY)\n"
)
sb.append(
" g.drawString(\"FRAME \" + (frameNo + 1) + \" / " + animationFrames.length + "\", 15, 20)\n"
)
sb.append(
" }\n"
)
sb.append(
"}\n\n"
)
sb.append(
"val timer = new Timer(" +
math.max(
20,
1000 / animationFps
) +
", new javax.swing.AbstractAction {\n"
)
sb.append(
" override def actionPerformed(e: ActionEvent): Unit = {\n"
)
sb.append(
" frameNo += 1\n"
)
sb.append(
" if (frameNo >= " +
animationFrames.length +
") {\n"
)
sb.append(
" frameNo = 0\n"
)
sb.append(
" }\n"
)
sb.append(
" panel.repaint()\n"
)
sb.append(
" }\n"
)
sb.append(
"})\n"
)
sb.append(
"w.add(panel)\n"
)
sb.append(
"w.setVisible(true)\n"
)
sb.append(
"timer.start()\n"
)
generatedAnimationCode =
sb.toString
if (codeArea != null) {
codeArea.setText(
generatedAnimationCode
)
}
setStatus(
"ANIMATION CODE GENERATED: " +
animationFrames.length +
" FRAMES"
)
}
// ============================================================
// VIDEO -> FRAME ANIMATION IMPORT
// ============================================================
def addVideoFramesFromFile(video: java.io.File, targetMaxFrames: Int): Unit = {
try {
importedVideoPath = video.getAbsolutePath
importedVideoName = video.getName
val tempDir = Files.createTempDirectory("ultra_video_frames_").toFile
val pattern = new java.io.File(tempDir, "frame_%05d.png")
val command = Array(
"ffmpeg", "-y", "-i", video.getAbsolutePath,
"-vf", "fps=" + animationFps,
"-frames:v", targetMaxFrames.toString,
pattern.getAbsolutePath
)
val pb = new ProcessBuilder(command: _*)
pb.redirectErrorStream(true)
val process = pb.start()
process.waitFor()
val files = tempDir.listFiles()
if (files == null || files.isEmpty) {
setStatus("VIDEO IMPORT FAILED - FFMPEG PRODUCED NO FRAMES")
return
}
val sorted = files.sortBy(_.getName)
animationFrames.clear()
animationImageFrames.clear()
animationCurrentFrame = 0
var fi = 0
while (fi < sorted.length) {
val img = javax.imageio.ImageIO.read(sorted(fi))
if (img != null) {
val frameItems = ArrayBuffer[DrawItem]()
val fit = math.min(CANVAS_W.toDouble / img.getWidth.toDouble, CANVAS_H.toDouble / img.getHeight.toDouble)
val dw = math.max(1, math.round(img.getWidth * fit).toInt)
val dh = math.max(1, math.round(img.getHeight * fit).toInt)
val copy = new BufferedImage(dw, dh, BufferedImage.TYPE_INT_ARGB)
val cg = copy.createGraphics()
cg.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
cg.drawImage(img, 0, 0, dw, dh, null)
cg.dispose()
// Store the frame as a special imported-image drawing item with a tiny marker path.
// The actual frame pixels are kept in videoFrameImages for exact code generation.
videoFrameImages += copy
}
fi += 1
}
if (videoFrameImages.nonEmpty) {
var dummy = 0
while (dummy < videoFrameImages.length) {
animationFrames += ArrayBuffer[DrawItem]()
dummy += 1
}
generateVideoAnimationCode()
setStatus("VIDEO IMPORTED: " + sorted.length + " FRAMES @ " + animationFps + " FPS")
}
} catch {
case ex: Throwable => setStatus("VIDEO IMPORT ERROR: " + ex.getMessage)
}
}
var videoFrameImages =
ArrayBuffer[BufferedImage]()
// Each animation frame may optionally contain one imported/pasted image.
// The image is rendered first, then normal vector drawings are drawn over it.
var animationImageFrames =
ArrayBuffer[BufferedImage]()
def ensureAnimationImageSlots(): Unit = {
while (animationImageFrames.length < animationFrames.length) {
animationImageFrames += null
}
while (animationImageFrames.length > animationFrames.length) {
animationImageFrames.remove(animationImageFrames.length - 1)
}
}
def setAnimationImageFrame(img: BufferedImage): Unit = {
if (img == null) return
ensureAnimationImageSlots()
if (animationFrames.isEmpty) {
animationFrames += animationClone(drawItems)
animationCurrentFrame = 0
}
ensureAnimationImageSlots()
animationImageFrames(animationCurrentFrame) = img
repaintCanvas()
}
def importImageToAnimationFrame(file: java.io.File): Unit = {
try {
val img = javax.imageio.ImageIO.read(file)
if (img == null) {
setStatus("ANIMATION IMAGE NOT SUPPORTED")
return
}
val copy = new BufferedImage(img.getWidth, img.getHeight, BufferedImage.TYPE_INT_ARGB)
val cg = copy.createGraphics()
cg.drawImage(img, 0, 0, null)
cg.dispose()
setAnimationImageFrame(copy)
importedImageOriginal = copy
importedImage = fitImageToCanvas(copy)
importedImagePath = file.getAbsolutePath
importedImageName = file.getName
generatedCode = generateExactImageCode(copy, file.getName)
if (codeArea != null) codeArea.setText(generatedCode)
setStatus("ANIMATION IMAGE IMPORTED: FRAME " + (animationCurrentFrame + 1) + " | " + copy.getWidth + "x" + copy.getHeight)
} catch {
case ex: Throwable => setStatus("ANIMATION IMAGE IMPORT ERROR: " + ex.getMessage)
}
}
def defaultMediaDirectory(): java.io.File = {
val home = new java.io.File(System.getProperty("user.home"))
val downloads = new java.io.File(home, "Downloads")
if (downloads.isDirectory) downloads else home
}
def chooseAndImportImageToAnimation(parent: JFrame): Unit = {
val chooser = new JFileChooser(defaultMediaDirectory())
chooser.setDialogTitle("IMPORT IMAGE INTO ANIMATION FRAME")
chooser.setFileFilter(new FileNameExtensionFilter("Images: PNG, JPG, JPEG, GIF, BMP", "png", "jpg", "jpeg", "gif", "bmp"))
if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
importImageToAnimationFrame(chooser.getSelectedFile)
}
}
def pasteImageToAnimationFrame(): Unit = {
try {
val cb = Toolkit.getDefaultToolkit.getSystemClipboard
val t = cb.getContents(null)
if (t != null && t.isDataFlavorSupported(DataFlavor.imageFlavor)) {
val raw = t.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
val img = bufferedImageFromAwtImage(raw)
if (img != null) {
setAnimationImageFrame(img)
importedImageOriginal = img
importedImage = fitImageToCanvas(img)
importedImagePath = "CLIPBOARD"
importedImageName = "Animation Clipboard Image"
generatedCode = generateExactImageCode(img, importedImageName)
if (codeArea != null) codeArea.setText(generatedCode)
setStatus("IMAGE PASTED INTO ANIMATION FRAME " + (animationCurrentFrame + 1))
return
}
}
setStatus("CLIPBOARD DOES NOT CONTAIN AN IMAGE")
} catch {
case ex: Throwable => setStatus("ANIMATION PASTE ERROR: " + ex.getMessage)
}
}
def clearAnimationImageFrame(): Unit = {
ensureAnimationImageSlots()
if (animationFrames.nonEmpty && animationCurrentFrame >= 0 && animationCurrentFrame < animationImageFrames.length) {
animationImageFrames(animationCurrentFrame) = null
setStatus("IMAGE REMOVED FROM FRAME " + (animationCurrentFrame + 1))
}
}
def animationImageDropHandler(): TransferHandler = {
new TransferHandler() {
override def canImport(support: TransferHandler.TransferSupport): Boolean = {
support.isDataFlavorSupported(DataFlavor.imageFlavor) ||
support.isDataFlavorSupported(DataFlavor.javaFileListFlavor)
}
override def importData(support: TransferHandler.TransferSupport): Boolean = {
try {
if (!canImport(support)) return false
val tr = support.getTransferable
if (tr.isDataFlavorSupported(DataFlavor.imageFlavor)) {
val raw = tr.getTransferData(DataFlavor.imageFlavor).asInstanceOf[java.awt.Image]
val img = bufferedImageFromAwtImage(raw)
if (img != null) {
setAnimationImageFrame(img)
importedImageOriginal = img
importedImage = fitImageToCanvas(img)
importedImagePath = "DRAGGED IMAGE"
importedImageName = "Animation Dropped Image"
generatedCode = generateExactImageCode(img, importedImageName)
if (codeArea != null) codeArea.setText(generatedCode)
setStatus("IMAGE DROPPED INTO ANIMATION FRAME " + (animationCurrentFrame + 1))
true
} else false
} else {
val data = tr.getTransferData(DataFlavor.javaFileListFlavor)
val files = data.asInstanceOf[java.util.List[_]]
if (files == null || files.isEmpty) return false
val it = files.iterator()
if (it.hasNext) {
val f = it.next().asInstanceOf[java.io.File]
if (isImageFile(f)) {
importImageToAnimationFrame(f)
true
} else false
} else false
}
} catch {
case ex: Throwable =>
setStatus("ANIMATION DROP ERROR: " + ex.getMessage)
false
}
}
}
}
def generateVideoAnimationCode(): Unit = {
if (videoFrameImages.isEmpty) return
val sb = new StringBuilder()
sb.append("// ULTRA LEGEND - VIDEO TO FRAME ANIMATION CODE\n")
sb.append("// Source: " + escapeScalaString(importedVideoName) + "\n")
sb.append("// Frames are embedded as PNG Base64. Audio is not embedded.\n\n")
sb.append("import java.awt.Graphics2D\n")
sb.append("import java.awt.image.BufferedImage\n")
sb.append("import java.io.ByteArrayInputStream\n")
sb.append("import java.util.Base64\n")
sb.append("import javax.imageio.ImageIO\n")
sb.append("import javax.swing.JFrame\n")
sb.append("import javax.swing.JPanel\n")
sb.append("import javax.swing.Timer\n")
sb.append("import javax.swing.WindowConstants\n\n")
sb.append("val frames = new Array[BufferedImage](" + videoFrameImages.length + ")\n")
var i = 0
while (i < videoFrameImages.length) {
val encoded = java.util.Base64.getEncoder.encodeToString(imageToPngBytes(videoFrameImages(i)))
sb.append("val frameData" + i + " = Array(\n")
var chunk = 0
while (chunk < encoded.length) {
val endChunk = math.min(chunk + 120, encoded.length)
sb.append(" \"")
sb.append(encoded.substring(chunk, endChunk))
sb.append("\"")
if (endChunk < encoded.length) sb.append(",")
sb.append("\n")
chunk = endChunk
}
sb.append(")\n")
sb.append("val frame" + i + " = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder.decode(frameData" + i + ".mkString)))\n")
sb.append("frames(" + i + ") = frame" + i + "\n")
i += 1
}
sb.append("val w = new JFrame(\"VIDEO FRAME ANIMATION\")\n")
sb.append("w.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)\n")
sb.append("w.setSize(" + CANVAS_W + ", " + (CANVAS_H + 35) + ")\n")
sb.append("w.setLocationRelativeTo(null)\n")
sb.append("var frameNo = 0\n")
sb.append("val panel = new JPanel {\n")
sb.append(" override def paintComponent(graphics: java.awt.Graphics): Unit = {\n")
sb.append(" super.paintComponent(graphics)\n")
sb.append(" val g = graphics.asInstanceOf[Graphics2D]\n")
sb.append(" g.drawImage(frames(frameNo), 0, 0, null)\n")
sb.append(" g.drawString(\"FRAME \" + (frameNo + 1) + \" / \" + frames.length, 12, 20)\n")
sb.append(" }\n")
sb.append("}\n")
sb.append("w.add(panel)\n")
sb.append("w.setVisible(true)\n")
sb.append("val timer = new Timer(" + math.max(20, 1000 / animationFps) + ", new javax.swing.AbstractAction {\n")
sb.append(" override def actionPerformed(e: java.awt.event.ActionEvent): Unit = {\n")
sb.append(" frameNo += 1\n")
sb.append(" if (frameNo >= frames.length) frameNo = 0\n")
sb.append(" panel.repaint()\n")
sb.append(" }\n")
sb.append("})\n")
sb.append("timer.start()\n")
generatedAnimationCode = sb.toString
if (codeArea != null) codeArea.setText(generatedAnimationCode)
}
// ============================================================
// OPEN INDEPENDENT ANIMATION WINDOW
// ============================================================
def openFrameAnimationStudio(): Unit = {
animationFrames.clear()
animationClipboard.clear()
videoFrameImages.clear()
animationImageFrames.clear()
animationCurrentFrame = 0
animationFrames +=
animationClone(drawItems)
animationImageFrames += null
val win =
new JFrame(
"ULTRA ANIMATION DRAW STUDIO - FRAME BY FRAME"
)
win.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE
)
win.setSize(
1500,
980
)
win.setLocationRelativeTo(
mainFrame
)
win.setLayout(
new BorderLayout()
)
var drawing = false
var moving = false
var selecting = false
var temp =
ArrayBuffer[Point]()
var pressPoint =
new Point(0, 0)
var originalSelection =
Vector.empty[Point]
var previewTimer: Timer =
null
var playing = false
var previewIndex = 0
var ghostMode = true
var currentTool = "DRAW"
var onionAlpha = 55
var animationImageMode = "FIT"
var animationImageZoom = 1.0
val frameLabel =
new JLabel(
"FRAME 1 / 1",
SwingConstants.CENTER
)
val modeLabel =
new JLabel(
"DRAW MODE"
)
val hintLabel =
new JLabel(
"DRAW: click-drag SELECT: move IMAGE: IMPORT / PASTE / CTRL+V / DROP FULLSCREEN / ZOOM / FIT / FILL"
)
val editor =
new JPanel {
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[Graphics2D]
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
)
g.setColor(
new Color(
218,
222,
232
)
)
g.fillRect(
0,
0,
getWidth,
getHeight
)
val ox =
80
val oy =
30
val old =
g.getTransform
g.translate(
ox,
oy
)
ensureAnimationImageSlots()
if (false) {
val vi =
if (playing) {
math.max(
0,
math.min(
videoFrameImages.length - 1,
previewIndex
)
)
} else {
math.max(
0,
math.min(
videoFrameImages.length - 1,
animationCurrentFrame
)
)
}
val vimg =
videoFrameImages(vi)
val vx =
(CANVAS_W - vimg.getWidth) / 2
val vy =
(CANVAS_H - vimg.getHeight) / 2
g.drawImage(
vimg,
vx,
vy,
null
)
}
if (animationFrames.nonEmpty && animationImageFrames.nonEmpty) {
val ai =
math.max(0, math.min(animationImageFrames.length - 1, if (playing) previewIndex else animationCurrentFrame))
val aimg = animationImageFrames(ai)
if (aimg != null) {
// Large image preview: FIT preserves aspect ratio; FILL covers the whole page; ZOOM enlarges.
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)
val fitScale = math.min(CANVAS_W.toDouble / aimg.getWidth.toDouble, CANVAS_H.toDouble / aimg.getHeight.toDouble)
val fillScale = math.max(CANVAS_W.toDouble / aimg.getWidth.toDouble, CANVAS_H.toDouble / aimg.getHeight.toDouble)
val baseScale = if (animationImageMode == "FILL") fillScale else fitScale
val finalScale = math.max(0.05, baseScale * animationImageZoom)
val aw = math.max(1, math.round(aimg.getWidth * finalScale).toInt)
val ah = math.max(1, math.round(aimg.getHeight * finalScale).toInt)
val ix = (CANVAS_W - aw) / 2
val iy = (CANVAS_H - ah) / 2
g.drawImage(aimg, ix, iy, aw, ah, null)
}
}
val activeSource =
if (
playing &&
animationFrames.nonEmpty
) {
animationFrames(
math.max(
0,
math.min(
animationFrames.length - 1,
previewIndex
)
)
)
} else {
drawItems
}
var ghostSource =
ArrayBuffer[DrawItem]()
if (
ghostMode &&
!playing &&
animationCurrentFrame > 0
) {
ghostSource =
animationFrames(
animationCurrentFrame - 1
)
}
paintFrameScene(
g,
activeSource,
ghostSource,
ghostMode && !playing,
if (playing) -1 else selectedIndex
)
if (onionAlpha > 0 && ghostMode && !playing) {
modeLabel.setText(
"DRAW MODE | ONION SKIN " +
onionAlpha + "%"
)
}
g.setTransform(old)
}
addMouseListener(
new MouseAdapter {
override def mousePressed(
e: MouseEvent
): Unit = {
if (playing) {
return
}
val p =
new Point(
math.max(
0,
math.min(
CANVAS_W - 1,
e.getX - 80
)
),
math.max(
0,
math.min(
CANVAS_H - 1,
e.getY - 30
)
)
)
if (
p.x < 0 ||
p.x >= CANVAS_W ||
p.y < 0 ||
p.y >= CANVAS_H
) {
return
}
if (currentTool == "SELECT") {
val hit =
findObject(p)
if (hit >= 0) {
selectedIndex = hit
moving = true
selecting = true
pressPoint = p
originalSelection =
drawItems(hit).points
} else {
selectedIndex = -1
}
} else {
drawing = true
temp.clear()
temp += p
selectedIndex = -1
}
repaint()
}
override def mouseReleased(
e: MouseEvent
): Unit = {
if (playing) {
return
}
if (moving && selecting) {
moving = false
selecting = false
animationSnapshot()
setStatus(
"FRAME " +
(animationCurrentFrame + 1) +
" OBJECT MOVED"
)
}
if (
drawing &&
temp.length >= 2
) {
val pts =
temp.toVector
val item =
DrawItem(
pts,
strokeColor,
fillColor,
closed(pts),
"FRAME DRAW",
true,
false
)
drawItems += item
selectedIndex =
drawItems.length - 1
animationSnapshot()
}
temp.clear()
drawing = false
repaint()
}
}
)
addMouseMotionListener(
new MouseMotionAdapter {
override def mouseDragged(
e: MouseEvent
): Unit = {
if (playing) {
return
}
val p =
new Point(
math.max(
0,
math.min(
CANVAS_W - 1,
e.getX - 80
)
),
math.max(
0,
math.min(
CANVAS_H - 1,
e.getY - 30
)
)
)
if (moving && selecting) {
val dx =
p.x - pressPoint.x
val dy =
p.y - pressPoint.y
if (
selectedIndex >= 0 &&
selectedIndex < drawItems.length
) {
val item =
drawItems(selectedIndex)
drawItems(selectedIndex) =
item.copy(
points =
originalSelection.map { q =>
new Point(
q.x + dx,
q.y + dy
)
}
)
}
} else if (drawing) {
if (temp.isEmpty || distance(temp.last, p) >= 2.0) {
temp += p
}
}
repaint()
}
}
)
}
editor.setCursor(
pencilCursor
)
editor.setTransferHandler(animationImageDropHandler())
editor.setFocusable(true)
editor.addKeyListener(
new KeyAdapter {
override def keyPressed(e: KeyEvent): Unit = {
if (e.isControlDown && e.getKeyCode == KeyEvent.VK_V) {
pasteImageToAnimationFrame()
editor.repaint()
}
}
}
)
// ----------------------------------------------------------
// BUTTONS
// ----------------------------------------------------------
val drawB =
button("DRAW")
val selectB =
button("SELECT / MOVE")
val copyB =
button("COPY DRAW")
val pasteB =
button("PASTE DRAW")
val prevB =
button("PREVIOUS")
val nextB =
button("NEXT FRAME")
val clearB =
button("CLEAR FRAME")
val deleteB =
button("DELETE SELECTED")
val playB =
button("PLAY")
val stopB =
button("STOP")
val codeB =
button("GENERATE ANIMATION CODE")
val importImageAnimB =
button("IMPORT IMAGE")
val pasteImageAnimB =
button("PASTE IMAGE")
val clearImageAnimB =
button("REMOVE IMAGE")
val fitImageAnimB =
button("FIT IMAGE")
val fillImageAnimB =
button("FILL IMAGE")
val zoomImageAnimB =
button("ZOOM IMAGE")
val doneB =
button("DONE")
val strokeB =
button("DRAW COLOR")
val fillB =
button("FILL COLOR")
val fpsMinusB =
button("FPS -")
val fpsPlusB =
button("FPS +")
val onionB =
button("ONION SKIN")
val opacityMinusB =
button("GHOST -")
val opacityPlusB =
button("GHOST +")
val fpsLabel =
new JLabel(
"FPS: " + animationFps
)
val timeline =
new JPanel(
new FlowLayout(
FlowLayout.LEFT,
3,
3
)
)
val top =
new JPanel(
new FlowLayout(
FlowLayout.LEFT,
4,
4
)
)
top.add(frameLabel)
top.add(drawB)
top.add(selectB)
top.add(copyB)
top.add(pasteB)
top.add(prevB)
top.add(nextB)
top.add(clearB)
top.add(deleteB)
top.add(playB)
top.add(stopB)
top.add(codeB)
top.add(importImageAnimB)
top.add(pasteImageAnimB)
top.add(clearImageAnimB)
top.add(fitImageAnimB)
top.add(fillImageAnimB)
top.add(zoomImageAnimB)
top.add(doneB)
top.add(strokeB)
top.add(fillB)
top.add(onionB)
top.add(opacityMinusB)
top.add(opacityPlusB)
top.add(fpsMinusB)
val soundB =
button("SOUND ON/OFF")
val chooseSoundB =
button("CHOOSE WAV")
val beepB =
button("TEST SOUND")
top.add(fpsPlusB)
top.add(fpsLabel)
top.add(soundB)
top.add(chooseSoundB)
top.add(beepB)
val commandField =
new JTextField(
18
)
val commandCombo =
new javax.swing.JComboBox[String](
animationCommandWords
)
val autoB =
button("AUTO ANIMATE")
val addShapeB =
button("ADD SHAPE")
val addObjectB =
button("ADD OBJECT")
val shapeCombo =
new javax.swing.JComboBox[String](
shapes
)
val objectCombo =
new javax.swing.JComboBox[String](
objects
)
val commandPanel =
new JPanel(
new FlowLayout(
FlowLayout.LEFT,
4,
4
)
)
commandPanel.add(
new JLabel(
"ANIMATION:")
)
commandPanel.add(
commandCombo
)
commandPanel.add(
commandField
)
commandPanel.add(
autoB
)
commandPanel.add(
new JLabel("SHAPE")
)
commandPanel.add(
shapeCombo
)
commandPanel.add(
addShapeB
)
commandPanel.add(
new JLabel("OBJECT")
)
commandPanel.add(
objectCombo
)
commandPanel.add(
addObjectB
)
commandPanel.add(
hintLabel
)
addShapeB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val selected =
shapeCombo.getSelectedItem
if (selected != null) {
addShape(
selected.toString
)
animationSnapshot()
refreshAll()
}
}
}
)
addObjectB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val selected =
objectCombo.getSelectedItem
if (selected != null) {
addObject(
selected.toString
)
animationSnapshot()
refreshAll()
}
}
}
)
// ----------------------------------------------------------
// TIMELINE
// ----------------------------------------------------------
def rebuildTimeline(): Unit = {
timeline.removeAll()
var i = 0
while (i < animationFrames.length) {
val frameIndex = i
val imageMarker =
if (i < animationImageFrames.length && animationImageFrames(i) != null) " IMG" else ""
val b =
new JButton(
"F" + (i + 1) + imageMarker
)
b.setFocusable(false)
b.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
animationSnapshot()
animationLoad(frameIndex)
refreshAll()
}
}
)
timeline.add(b)
i += 1
}
timeline.revalidate()
timeline.repaint()
}
def refreshAll(): Unit = {
ensureAnimationImageSlots()
frameLabel.setText(
"FRAME " +
(animationCurrentFrame + 1) +
" / " +
math.max(
1,
animationFrames.length
)
)
fpsLabel.setText(
"FPS: " +
animationFps
)
rebuildTimeline()
editor.repaint()
}
// ----------------------------------------------------------
// MODE
// ----------------------------------------------------------
drawB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
currentTool = "DRAW"
modeLabel.setText(
"DRAW MODE"
)
}
}
)
selectB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
currentTool = "SELECT"
modeLabel.setText(
"SELECT / MOVE MODE"
)
}
}
)
// ----------------------------------------------------------
// COPY / PASTE
// ----------------------------------------------------------
copyB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
animationCopy()
playStudioBeep()
modeLabel.setText(
"COPIED " +
animationClipboard.length +
" DRAWINGS"
)
}
}
)
pasteB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
animationPaste()
playStudioBeep()
animationSnapshot()
currentTool = "SELECT"
modeLabel.setText(
"PASTED - SELECT / MOVE IT"
)
editor.repaint()
}
}
)
// ----------------------------------------------------------
// NEXT / PREVIOUS
// ----------------------------------------------------------
nextB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
animationNextFrame()
playStudioBeep()
refreshAll()
modeLabel.setText(
"NEW FRAME " +
(animationCurrentFrame + 1) +
" - DRAW OR PASTE"
)
}
}
)
prevB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
animationSnapshot()
if (animationCurrentFrame > 0) {
animationCurrentFrame -= 1
animationLoad(
animationCurrentFrame
)
}
refreshAll()
}
}
)
clearB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
drawItems.clear()
selectedIndex = -1
animationSnapshot()
editor.repaint()
}
}
)
deleteB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
deleteSelected()
animationSnapshot()
refreshAll()
}
}
)
// ----------------------------------------------------------
// COLORS
// ----------------------------------------------------------
strokeB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val c =
JColorChooser.showDialog(
win,
"DRAW COLOR",
strokeColor
)
if (c != null) {
strokeColor = c
modeLabel.setText(
"DRAW COLOR RGB(" +
c.getRed +
"," +
c.getGreen +
"," +
c.getBlue +
")"
)
}
}
}
)
fillB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val c =
JColorChooser.showDialog(
win,
"FILL COLOR",
fillColor
)
if (c != null) {
fillColor = c
if (
selectedIndex >= 0 &&
selectedIndex < drawItems.length
) {
fillSelected()
animationSnapshot()
}
editor.repaint()
}
}
}
)
// ----------------------------------------------------------
// ONION SKIN
// ----------------------------------------------------------
onionB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
ghostMode = !ghostMode
modeLabel.setText(
if (ghostMode) {
"ONION SKIN ON"
} else {
"ONION SKIN OFF"
}
)
editor.repaint()
}
}
)
opacityMinusB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
onionAlpha =
math.max(
10,
onionAlpha - 10
)
editor.repaint()
}
}
)
opacityPlusB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
onionAlpha =
math.min(
120,
onionAlpha + 10
)
editor.repaint()
}
}
)
// ----------------------------------------------------------
// FPS
// ----------------------------------------------------------
fpsMinusB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
animationFps =
math.max(
2,
animationFps - 1
)
refreshAll()
}
}
)
fpsPlusB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
animationFps =
math.min(
30,
animationFps + 1
)
refreshAll()
}
}
)
// ----------------------------------------------------------
// PLAY
// ----------------------------------------------------------
playB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
animationSnapshot()
if (animationFrames.length < 2) {
modeLabel.setText(
"MAKE 2 OR MORE FRAMES FIRST"
)
return
}
if (previewTimer != null) {
previewTimer.stop()
}
playing = true
previewIndex = 0
previewTimer =
new Timer(
math.max(
20,
1000 / animationFps
),
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
previewIndex += 1
if (
previewIndex >=
animationFrames.length
) {
previewIndex = 0
}
editor.repaint()
}
}
)
previewTimer.start()
modeLabel.setText(
"PLAYING ALL FRAMES"
)
}
}
)
stopB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
if (previewTimer != null) {
previewTimer.stop()
}
playing = false
animationLoad(
animationCurrentFrame
)
modeLabel.setText(
"PLAY STOPPED"
)
editor.repaint()
}
}
)
// ----------------------------------------------------------
// AUTO ANIMATE
// ----------------------------------------------------------
autoB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val typed =
commandField
.getText
.trim
.toUpperCase
val selected =
commandCombo.getSelectedItem
var command =
"NONE"
if (typed.nonEmpty) {
command = typed
} else if (selected != null) {
command =
selected.toString.toUpperCase
}
if (command != "NONE") {
animationSnapshot()
var base =
animationClone(drawItems)
var step = 1
while (step <= 12) {
val next =
animationTransform(
base,
command,
step
)
animationFrames +=
animationClone(next)
base = next
step += 1
}
animationCurrentFrame =
animationFrames.length - 1
drawItems.clear()
drawItems ++= animationClone(base)
selectedIndex = -1
refreshAll()
modeLabel.setText(
"AUTO ANIMATION: " +
command +
" | 12 NEW FRAMES"
)
}
}
}
)
// ----------------------------------------------------------
// SOUND
// ----------------------------------------------------------
soundB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
soundEnabled = !soundEnabled
if (soundEnabled) {
modeLabel.setText("SOUND ON")
} else {
modeLabel.setText("SOUND OFF")
}
}
}
)
chooseSoundB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val chooser =
new JFileChooser()
chooser.setDialogTitle(
"CHOOSE WAV SOUND"
)
if (
chooser.showOpenDialog(win) ==
JFileChooser.APPROVE_OPTION
) {
selectedSoundFile =
chooser.getSelectedFile.getAbsolutePath
modeLabel.setText(
"SOUND: " +
chooser.getSelectedFile.getName
)
playStudioBeep()
}
}
}
)
beepB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
playStudioBeep()
modeLabel.setText("TEST SOUND PLAYED")
}
}
)
// ----------------------------------------------------------
// CODE / DONE
// ----------------------------------------------------------
codeB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
if (previewTimer != null) {
previewTimer.stop()
}
playing = false
generateFrameAnimationStudioCode()
playStudioBeep()
modeLabel.setText(
"ANIMATION CODE READY - CHECK CODE PANEL"
)
}
}
)
importImageAnimB.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
chooseAndImportImageToAnimation(win)
refreshAll()
editor.requestFocusInWindow()
}
}
)
pasteImageAnimB.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
pasteImageToAnimationFrame()
refreshAll()
editor.requestFocusInWindow()
}
}
)
clearImageAnimB.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
clearAnimationImageFrame()
editor.repaint()
}
}
)
fitImageAnimB.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
animationImageMode = "FIT"
animationImageZoom = 1.0
modeLabel.setText("IMAGE MODE: FIT")
editor.repaint()
}
})
fillImageAnimB.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
animationImageMode = "FILL"
animationImageZoom = 1.0
modeLabel.setText("IMAGE MODE: FILL FULL PAGE")
editor.repaint()
}
})
zoomImageAnimB.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
animationImageZoom = math.min(5.0, animationImageZoom + 0.25)
modeLabel.setText("IMAGE ZOOM: " + animationImageZoom)
editor.repaint()
}
})
doneB.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
if (previewTimer != null) {
previewTimer.stop()
}
playing = false
generateFrameAnimationStudioCode()
modeLabel.setText(
"DONE - FULL FRAME ANIMATION CODE GENERATED"
)
}
}
)
// ----------------------------------------------------------
// BOTTOM PANEL
// ----------------------------------------------------------
val bottom =
new JPanel(
new BorderLayout()
)
bottom.add(
modeLabel,
BorderLayout.NORTH
)
bottom.add(
new JScrollPane(
timeline
),
BorderLayout.CENTER
)
bottom.add(
commandPanel,
BorderLayout.SOUTH
)
win.add(
top,
BorderLayout.NORTH
)
win.add(
new JScrollPane(
editor
),
BorderLayout.CENTER
)
win.add(
bottom,
BorderLayout.SOUTH
)
win.addWindowListener(
new java.awt.event.WindowAdapter {
override def windowClosed(
e: java.awt.event.WindowEvent
): Unit = {
if (previewTimer != null) {
previewTimer.stop()
}
animationStudioOpen =
false
}
}
)
animationStudioOpen =
true
rebuildTimeline()
refreshAll()
win.setVisible(true)
}
// ============================================================
// ULTRA LAPTOP LAUNCHER
// REALISTIC LAPTOP SHELL + BRAND/COLOR + KEYBOARD + MOUSE
// ============================================================
def launchUltraLaptop(): Unit = {
// ==========================================================
// VIRTUAL LAPTOP / WINDOWS-STYLE DESKTOP STATE
// ==========================================================
var laptopColor = new Color(48, 52, 60)
var laptopBrand = "ULTRA X9"
var powered = false
var desktopMode = "DESKTOP"
var cursorX = 430
var cursorY = 170
var startOpen = false
val keyMap =
new scala.collection.mutable.HashMap[Int, JButton]()
val laptopFrame =
new JFrame("" + laptopBrand + " - ULTRA WINDOWS LAPTOP")
laptopFrame.setDefaultCloseOperation(
WindowConstants.EXIT_ON_CLOSE
)
laptopFrame.setSize(1380, 940)
laptopFrame.setMinimumSize(new Dimension(1120, 800))
laptopFrame.setLocationRelativeTo(null)
laptopFrame.setLayout(new BorderLayout(8, 8))
// ==========================================================
// TOP CONTROL BAR
// ==========================================================
val brandField =
new JTextField("ULTRA X9", 14)
val colorButton =
button("LAPTOP COLOR")
val powerButton =
button("POWER ON")
val resetButton =
button("RESET")
val topStatus =
new JLabel("POWER OFF | Configure your laptop first")
topStatus.setFont(new Font("Arial", Font.BOLD, 13))
val topBar =
new JPanel(new FlowLayout(FlowLayout.CENTER, 8, 6))
topBar.setBackground(new Color(20, 23, 30))
topBar.add(new JLabel("BRAND"))
topBar.add(brandField)
topBar.add(colorButton)
topBar.add(powerButton)
topBar.add(resetButton)
topBar.add(topStatus)
// ==========================================================
// VIRTUAL SCREEN
// ==========================================================
val screen =
new JPanel {
setLayout(null)
setFocusable(true)
setBackground(Color.BLACK)
override def paintComponent(graphics: Graphics): Unit = {
super.paintComponent(graphics)
val g = graphics.asInstanceOf[Graphics2D]
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
)
val screenX = 24
val screenY = 22
val screenW = getWidth - 48
val screenH = getHeight - 64
g.setColor(new Color(10, 12, 17))
g.fillRoundRect(0, 0, getWidth, getHeight, 30, 30)
if (!powered) {
g.setColor(new Color(24, 28, 38))
g.fillRoundRect(screenX, screenY, screenW, screenH, 22, 22)
g.setColor(laptopColor)
g.fillOval(getWidth / 2 - 8, 8, 16, 16)
g.setColor(Color.WHITE)
g.setFont(new Font("Arial", Font.BOLD, 42))
val title = laptopBrand
g.drawString(title, getWidth / 2 - 160, getHeight / 2 - 36)
g.setFont(new Font("Arial", Font.PLAIN, 18))
g.setColor(new Color(190, 198, 215))
g.drawString(
"Press POWER ON to start your Windows-style laptop",
getWidth / 2 - 250,
getHeight / 2 + 8
)
g.setColor(laptopColor.brighter())
g.setFont(new Font("Arial", Font.BOLD, 16))
g.drawString("BRAND: " + laptopBrand, getWidth / 2 - 95, getHeight / 2 + 50)
} else {
val topColor = laptopColor.brighter()
val bottomColor = new Color(11, 20, 46)
g.setPaint(
new GradientPaint(
0,
screenY,
topColor,
0,
screenY + screenH,
bottomColor
)
)
g.fillRoundRect(screenX, screenY, screenW, screenH, 22, 22)
// Wallpaper bubbles / light
g.setColor(new Color(255, 255, 255, 28))
g.fillOval(screenX + screenW - 320, screenY + 50, 250, 250)
g.setColor(new Color(255, 255, 255, 20))
g.fillOval(screenX + 70, screenY + 90, 180, 180)
// Desktop title
g.setColor(Color.WHITE)
g.setFont(new Font("Arial", Font.BOLD, 21))
g.drawString(laptopBrand, screenX + 24, screenY + 34)
g.setFont(new Font("Arial", Font.PLAIN, 12))
g.drawString(
"ULTRA WINDOWS DESKTOP",
screenX + 24,
screenY + 52
)
// Taskbar
val taskY = screenY + screenH - 48
g.setColor(new Color(8, 11, 20, 225))
g.fillRect(screenX + 5, taskY, screenW - 10, 42)
g.setColor(Color.WHITE)
g.setFont(new Font("Arial", Font.BOLD, 13))
g.drawString("START", screenX + 18, taskY + 26)
g.drawString("?", screenX + 82, taskY + 26)
g.drawString("?", screenX + 116, taskY + 26)
g.setFont(new Font("Arial", Font.PLAIN, 12))
g.drawString(
desktopMode,
screenX + 155,
taskY + 26
)
// Clock
val now = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date())
g.drawString(now, screenX + screenW - 84, taskY + 25)
// Custom cursor
val cx = math.max(screenX + 3, math.min(screenX + screenW - 25, cursorX))
val cy = math.max(screenY + 3, math.min(screenY + screenH - 40, cursorY))
val arrow = new Path2D.Double()
arrow.moveTo(cx, cy)
arrow.lineTo(cx + 2, cy + 27)
arrow.lineTo(cx + 10, cy + 20)
arrow.lineTo(cx + 19, cy + 33)
arrow.lineTo(cx + 25, cy + 29)
arrow.lineTo(cx + 16, cy + 17)
arrow.lineTo(cx + 27, cy + 17)
arrow.closePath()
g.setColor(Color.BLACK)
g.fill(arrow)
g.translate(1, 1)
g.setColor(Color.WHITE)
g.fill(arrow)
g.translate(-1, -1)
}
}
}
// ==========================================================
// DESKTOP APP HELPER
// ==========================================================
def iconButton(title: String, x: Int, y: Int): JButton = {
val b = button(title)
b.setSize(150, 56)
b.setLocation(x, y)
b.setFont(new Font("Arial", Font.BOLD, 12))
b.setFocusable(false)
b
}
val paintApp = iconButton("PAINT STUDIO", 90, 110)
val animationApp = iconButton("ANIMATION", 260, 110)
val gameApp = iconButton("GAME STUDIO", 430, 110)
val codeApp = iconButton("CODE DESK", 600, 110)
val notesApp = iconButton("NOTEPAD", 770, 110)
val calcApp = iconButton("CALCULATOR", 90, 185)
val browserApp = iconButton("WEB BROWSER", 260, 185)
val settingsApp = iconButton("SETTINGS", 430, 185)
val clockApp = iconButton("CLOCK", 600, 185)
// Newly integrated apps and game. These buttons belong to the simulated desktop.
val liveChatApp = iconButton("YADNESH LIVE CHAT", 90, 260)
val oldHouseGameApp = iconButton("OLD HOUSE 3D", 260, 260)
val babasahebStudioApp = iconButton("BABASAHEB 3D", 430, 260)
val cricketLiveApp = iconButton("CRICKET LIVE", 600, 260)
val desktopApps = Array(
paintApp,
animationApp,
gameApp,
codeApp,
notesApp,
calcApp,
browserApp,
settingsApp,
clockApp,
liveChatApp,
oldHouseGameApp,
babasahebStudioApp,
cricketLiveApp
)
var i0 = 0
while (i0 < desktopApps.length) {
desktopApps(i0).setVisible(false)
screen.add(desktopApps(i0))
i0 += 1
}
// Start panel (overlay-like Swing panel on screen)
val startPanel = new JPanel()
startPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 8, 8))
startPanel.setBounds(20, 245, 560, 275)
startPanel.setBackground(new Color(16, 20, 31, 245))
val startTitle = new JLabel(" ULTRA START MENU ")
startTitle.setForeground(Color.WHITE)
startTitle.setFont(new Font("Arial", Font.BOLD, 16))
startPanel.add(startTitle)
val startCalc = button("Calculator")
val startNotes = button("Notepad")
val startSettings = button("Settings")
val startPaint = button("Paint")
val startGame = button("Game Studio")
val startAppsLibrary = button("Apps Library")
val startGamesLibrary = button("Games Library")
startPanel.add(startCalc)
startPanel.add(startNotes)
startPanel.add(startSettings)
startPanel.add(startPaint)
startPanel.add(startGame)
startPanel.add(startAppsLibrary)
startPanel.add(startGamesLibrary)
startPanel.setVisible(false)
screen.add(startPanel)
def showDesktopIcons(show: Boolean): Unit = {
var ix = 0
while (ix < desktopApps.length) {
desktopApps(ix).setVisible(show)
ix += 1
}
}
// ==========================================================
// GENERIC WINDOW BUILDER
// ==========================================================
def appWindow(title: String, width: Int, height: Int): JFrame = {
val win = new JFrame(laptopBrand + " - " + title)
win.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
win.setSize(width, height)
win.setLocationRelativeTo(laptopFrame)
win.setLayout(new BorderLayout())
win
}
// ==========================================================
// APP / GAME CATALOGS
// ==========================================================
def openAppLibrary(initialSection: String): Unit = {
val win = appWindow(if (initialSection == "GAMES") "GAMES LIBRARY" else "APPS LIBRARY", 760, 610)
val tabs = new JTabbedPane()
val appsPanel = new JPanel(new java.awt.GridLayout(0, 2, 10, 10))
val gamesPanel = new JPanel(new java.awt.GridLayout(0, 2, 10, 10))
appsPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(12, 12, 12, 12))
gamesPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(12, 12, 12, 12))
def addEntry(panel: JPanel, title: String, detail: String, run: () => Unit): Unit = {
val card = new JPanel(new BorderLayout(6, 6))
card.setBorder(javax.swing.BorderFactory.createCompoundBorder(
javax.swing.BorderFactory.createLineBorder(new Color(80, 102, 143), 1),
javax.swing.BorderFactory.createEmptyBorder(9, 9, 9, 9)
))
val heading = new JLabel(title)
heading.setFont(new Font("Arial", Font.BOLD, 14))
val description = new JLabel("<html><body style='width:240px'>" + detail + "</body></html>")
description.setFont(new Font("Arial", Font.PLAIN, 11))
val launch = button("OPEN")
launch.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
win.dispose()
try run() catch { case ex: Throwable => javax.swing.JOptionPane.showMessageDialog(laptopFrame, "Could not launch " + title + ": " + ex.getMessage, "Launch Error", javax.swing.JOptionPane.ERROR_MESSAGE) }
}
})
card.add(heading, BorderLayout.NORTH)
card.add(description, BorderLayout.CENTER)
card.add(launch, BorderLayout.SOUTH)
panel.add(card)
}
addEntry(appsPanel, "Paint Studio", "Draw, shape, import media and edit artwork.", () => { if (mainFrame != null) { mainFrame.setLocationRelativeTo(laptopFrame); mainFrame.setVisible(true); canvas.requestFocusInWindow() } })
addEntry(appsPanel, "Animation Studio", "Create frame-based animations.", () => openFrameAnimationStudio())
addEntry(appsPanel, "Game Studio", "Open the built-in game creator.", () => openGameStudio())
addEntry(appsPanel, "Code Desk", "Open the coding workspace.", () => { if (mainFrame != null) { mainFrame.setLocationRelativeTo(laptopFrame); mainFrame.setVisible(true); if (codeArea != null) codeArea.requestFocusInWindow() } })
addEntry(appsPanel, "Notepad", "Create, open and save text documents.", () => openNotepad())
addEntry(appsPanel, "Calculator", "Basic expression calculator.", () => openCalculator())
addEntry(appsPanel, "Web Browser", "Open a URL in the system browser.", () => openBrowser())
addEntry(appsPanel, "Settings", "Change laptop theme and brand.", () => openSettings())
addEntry(appsPanel, "Clock", "Live clock and date.", () => openClock())
addEntry(appsPanel, "Yadnesh Live Chat", "Friends, requests, profiles and LAN chat.", () => openYadneshLiveChat())
addEntry(appsPanel, "Babasaheb 3D Statue Studio", "Inspect original procedural 3D statue models.", () => openBabasahebStudio())
addEntry(appsPanel, "Cricket Live Center", "Cricket live-score dashboard and source link.", () => openCricketLiveCenter())
addEntry(gamesPanel, "Old House 3D", "First-person exploration, locked exit, key and escape objective.", () => openOldHouseGame())
addEntry(gamesPanel, "Game Studio", "Create and preview your own games.", () => openGameStudio())
tabs.addTab("Apps", new JScrollPane(appsPanel))
tabs.addTab("Games", new JScrollPane(gamesPanel))
tabs.setSelectedIndex(if (initialSection == "GAMES") 1 else 0)
win.add(tabs, BorderLayout.CENTER)
win.setVisible(true)
}
// ==========================================================
// NOTEPAD - REAL EDITING + OPEN + SAVE + NEW
// ==========================================================
def openNotepad(): Unit = {
val win = appWindow("NOTEPAD", 850, 600)
val text = new JTextArea()
text.setFont(new Font("Monospaced", Font.PLAIN, 16))
text.setLineWrap(true)
text.setWrapStyleWord(true)
val top = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 6))
val newB = button("NEW")
val openB = button("OPEN")
val saveB = button("SAVE AS")
val info = new JLabel("Ready")
top.add(newB)
top.add(openB)
top.add(saveB)
top.add(info)
newB.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
text.setText("")
info.setText("New document")
}
})
openB.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
val chooser = new JFileChooser()
if (chooser.showOpenDialog(win) == JFileChooser.APPROVE_OPTION) {
val source = scala.io.Source.fromFile(chooser.getSelectedFile, "UTF-8")
try {
text.setText(source.mkString)
} finally {
source.close()
}
info.setText("Opened: " + chooser.getSelectedFile.getName)
}
}
})
saveB.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
val chooser = new JFileChooser()
if (chooser.showSaveDialog(win) == JFileChooser.APPROVE_OPTION) {
val out = new java.io.PrintWriter(chooser.getSelectedFile, "UTF-8")
try {
out.print(text.getText)
} finally {
out.close()
}
info.setText("Saved: " + chooser.getSelectedFile.getName)
}
}
})
win.add(top, BorderLayout.NORTH)
win.add(new JScrollPane(text), BorderLayout.CENTER)
win.setVisible(true)
text.requestFocusInWindow()
}
// ==========================================================
// CALCULATOR - REAL BUTTON INPUT + SAFE BASIC EXPRESSION MATH
// ==========================================================
def openCalculator(): Unit = {
val win = appWindow("CALCULATOR", 400, 530)
val display = new JTextField("0")
display.setHorizontalAlignment(SwingConstants.RIGHT)
display.setFont(new Font("Arial", Font.BOLD, 28))
display.setEditable(false)
val grid = new JPanel(new java.awt.GridLayout(5, 4, 6, 6))
val values = Array(
"7","8","9","/",
"4","5","6","*",
"1","2","3","-",
"0",".","=","+",
"C","(",")","%"
)
var current = ""
def setDisplay(s: String): Unit = {
display.setText(if (s.isEmpty) "0" else s)
}
def calculateSimple(expr: String): Double = {
val clean = expr.replace(" ", "")
def numbersAndOps: Vector[String] = {
val out = ArrayBuffer[String]()
var token = ""
var j = 0
while (j < clean.length) {
val c = clean.charAt(j)
if ((c >= '0' && c <= '9') || c == '.') {
token += c
} else {
if (token.nonEmpty) out += token
token = ""
out += c.toString
}
j += 1
}
if (token.nonEmpty) out += token
out.toVector
}
val tokens = numbersAndOps
val nums = ArrayBuffer[Double]()
val ops = ArrayBuffer[String]()
var k = 0
while (k < tokens.length) {
if (tokens(k) != "+" && tokens(k) != "-" && tokens(k) != "*" && tokens(k) != "/") {
nums += tokens(k).toDouble
} else {
ops += tokens(k)
}
k += 1
}
if (nums.isEmpty) 0.0
else {
var result = nums(0)
var n = 1
var o = 0
while (n < nums.length && o < ops.length) {
val op = ops(o)
if (op == "+") result += nums(n)
else if (op == "-") result -= nums(n)
else if (op == "*") result *= nums(n)
else if (op == "/") {
if (nums(n) == 0.0) throw new ArithmeticException("Cannot divide by zero")
result /= nums(n)
}
n += 1
o += 1
}
result
}
}
var ci = 0
while (ci < values.length) {
val label = values(ci)
val b = button(label)
b.setFont(new Font("Arial", Font.BOLD, 17))
b.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (label == "C") {
current = ""
setDisplay(current)
} else if (label == "=") {
try {
val result = calculateSimple(current)
current = if (result % 1.0 == 0.0) result.toLong.toString else result.toString
setDisplay(current)
} catch {
case _: Throwable =>
current = ""
setDisplay("ERROR")
}
} else if (label == "%") {
try {
val result = calculateSimple(current) / 100.0
current = result.toString
setDisplay(current)
} catch {
case _: Throwable =>
setDisplay("ERROR")
}
} else {
if (display.getText == "ERROR") current = ""
current += label
setDisplay(current)
}
}
})
grid.add(b)
ci += 1
}
win.add(display, BorderLayout.NORTH)
win.add(grid, BorderLayout.CENTER)
win.setVisible(true)
}
// ==========================================================
// WEB BROWSER - REAL SYSTEM BROWSER OPEN + URL ENTRY
// ==========================================================
def openBrowser(): Unit = {
val win = appWindow("WEB BROWSER", 850, 560)
val address = new JTextField("https://www.google.com")
val goB = button("GO")
val backB = button("HOME")
val systemB = button("OPEN IN SYSTEM BROWSER")
val message = new JTextArea()
message.setEditable(false)
message.setFont(new Font("Arial", Font.PLAIN, 16))
message.setLineWrap(true)
message.setWrapStyleWord(true)
message.setText(
"ULTRA WEB BROWSER\n\n" +
"Type a web address above. GO opens it in the computer's default browser.\n\n" +
"Example: https://www.google.com\nExample: https://www.wikipedia.org"
)
def browse(): Unit = {
try {
var u = address.getText.trim
if (!u.contains("://")) u = "https://" + u
java.awt.Desktop.getDesktop.browse(new java.net.URI(u))
message.setText("Opened in the system browser:\n\n" + u)
} catch {
case ex: Throwable => message.setText("Could not open URL:\n\n" + ex.getMessage)
}
}
goB.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = browse()
})
systemB.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = browse()
})
backB.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
address.setText("https://www.google.com")
}
})
val top = new JPanel(new BorderLayout(6, 6))
val controls = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 4))
controls.add(goB)
controls.add(backB)
controls.add(systemB)
top.add(address, BorderLayout.CENTER)
top.add(controls, BorderLayout.SOUTH)
win.add(top, BorderLayout.NORTH)
win.add(new JScrollPane(message), BorderLayout.CENTER)
win.setVisible(true)
}
// ==========================================================
// SETTINGS - LIVE THEME / BRAND SETTINGS
// ==========================================================
def openSettings(): Unit = {
val win = appWindow("SETTINGS", 600, 430)
val panel = new JPanel(new java.awt.GridLayout(0, 1, 8, 8))
val brand = new JTextField(laptopBrand)
val colorInfo = new JLabel(
"Current RGB: " + laptopColor.getRed + ", " + laptopColor.getGreen + ", " + laptopColor.getBlue
)
val choose = button("CHANGE THEME COLOR")
val applyBrand = button("APPLY BRAND")
val resetTheme = button("RESET THEME")
val keyboardInfo = new JLabel("Keyboard mirror: ON | Mouse cursor: ON")
applyBrand.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
val t = brand.getText.trim
laptopBrand = if (t.isEmpty) "ULTRA X9" else t
laptopFrame.setTitle(laptopBrand + " - ULTRA WINDOWS LAPTOP")
topStatus.setText("BRAND UPDATED: " + laptopBrand)
screen.repaint()
}
})
choose.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
val c = JColorChooser.showDialog(win, "CHOOSE WINDOWS THEME COLOR", laptopColor)
if (c != null) {
laptopColor = c
colorInfo.setText(
"Current RGB: " + laptopColor.getRed + ", " + laptopColor.getGreen + ", " + laptopColor.getBlue
)
screen.repaint()
}
}
})
resetTheme.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
laptopColor = new Color(48, 52, 60)
brand.setText("ULTRA X9")
laptopBrand = "ULTRA X9"
colorInfo.setText("Current RGB: 48, 52, 60")
screen.repaint()
}
})
panel.add(new JLabel("WINDOWS STYLE SETTINGS"))
panel.add(new JLabel("Laptop Brand"))
panel.add(brand)
panel.add(applyBrand)
panel.add(choose)
panel.add(colorInfo)
panel.add(resetTheme)
panel.add(keyboardInfo)
win.add(panel, BorderLayout.CENTER)
win.setVisible(true)
}
// ==========================================================
// CLOCK
// ==========================================================
def openClock(): Unit = {
val win = appWindow("CLOCK", 420, 260)
val label = new JLabel("00:00:00", SwingConstants.CENTER)
label.setFont(new Font("Arial", Font.BOLD, 44))
val dateLabel = new JLabel("", SwingConstants.CENTER)
dateLabel.setFont(new Font("Arial", Font.PLAIN, 17))
val timer = new Timer(500, new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
label.setText(new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date()))
dateLabel.setText(new java.text.SimpleDateFormat("EEEE, dd MMMM yyyy").format(new java.util.Date()))
}
})
timer.start()
win.add(label, BorderLayout.CENTER)
win.add(dateLabel, BorderLayout.SOUTH)
win.addWindowListener(new java.awt.event.WindowAdapter {
override def windowClosed(e: java.awt.event.WindowEvent): Unit = timer.stop()
})
win.setVisible(true)
}
// ==========================================================
// INTEGRATED APP LAUNCHERS
// ==========================================================
def openYadneshLiveChat(): Unit = {
desktopMode = "YADNESH LIVE CHAT"
topStatus.setText("OPENING YADNESH LIVE CHAT")
screen.repaint()
LiveChatApp.startApp()
}
def openOldHouseGame(): Unit = {
desktopMode = "OLD HOUSE 3D"
topStatus.setText("OLD HOUSE 3D GAME OPEN")
screen.repaint()
OldHouse3DImproved.start()
}
def openBabasahebStudio(): Unit = {
desktopMode = "BABASAHEB 3D STATUE STUDIO"
topStatus.setText("BABASAHEB 3D STATUE STUDIO OPEN")
screen.repaint()
Babasaheb3DStatue.start()
}
def openCricketLiveCenter(): Unit = {
desktopMode = "CRICKET LIVE CENTER"
topStatus.setText("CRICKET LIVE CENTER OPEN")
screen.repaint()
YTRCricketLiveCenterApp.start()
}
// ==========================================================
// APP ACTIONS
// ==========================================================
paintApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (mainFrame != null) {
mainFrame.setLocationRelativeTo(laptopFrame)
mainFrame.setVisible(true)
canvas.requestFocusInWindow()
}
desktopMode = "PAINT"
topStatus.setText("PAINT STUDIO OPEN")
screen.repaint()
}
})
animationApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
openFrameAnimationStudio()
desktopMode = "ANIMATION"
topStatus.setText("ANIMATION STUDIO OPEN")
screen.repaint()
}
})
gameApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
openGameStudio()
desktopMode = "GAME STUDIO"
topStatus.setText("GAME STUDIO OPEN")
screen.repaint()
}
})
codeApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (mainFrame != null) {
mainFrame.setLocationRelativeTo(laptopFrame)
mainFrame.setVisible(true)
if (codeArea != null) codeArea.requestFocusInWindow()
}
desktopMode = "CODE DESK"
topStatus.setText("CODE DESK OPEN")
screen.repaint()
}
})
notesApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = openNotepad()
})
calcApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = openCalculator()
})
browserApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = openBrowser()
})
settingsApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = openSettings()
})
clockApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = openClock()
})
liveChatApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = openYadneshLiveChat()
})
oldHouseGameApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = openOldHouseGame()
})
babasahebStudioApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = openBabasahebStudio()
})
cricketLiveApp.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = openCricketLiveCenter()
})
// Start menu shortcuts
startCalc.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
startPanel.setVisible(false)
openCalculator()
}
})
startNotes.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
startPanel.setVisible(false)
openNotepad()
}
})
startSettings.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
startPanel.setVisible(false)
openSettings()
}
})
startPaint.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
startPanel.setVisible(false)
if (mainFrame != null) mainFrame.setVisible(true)
}
})
startGame.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
startPanel.setVisible(false)
openGameStudio()
}
})
startAppsLibrary.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
startPanel.setVisible(false)
openAppLibrary("APPS")
}
})
startGamesLibrary.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
startPanel.setVisible(false)
openAppLibrary("GAMES")
}
})
// ==========================================================
// LAPTOP BASE REFERENCE
// Declared before listeners to avoid Scala forward-reference errors.
// It is fully built near the end of the setup section.
// ==========================================================
var base: JPanel = null
// ==========================================================
// COLOR BUTTON
// ==========================================================
colorButton.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
val selected = JColorChooser.showDialog(
laptopFrame,
"CHOOSE LAPTOP / WINDOWS COLOR",
laptopColor
)
if (selected != null) {
laptopColor = selected
screen.repaint()
topBar.setBackground(laptopColor.darker())
base.setBackground(laptopColor.darker())
topStatus.setText(
"THEME: RGB " +
laptopColor.getRed + "," +
laptopColor.getGreen + "," +
laptopColor.getBlue
)
}
}
})
brandField.addKeyListener(new KeyAdapter {
override def keyReleased(e: KeyEvent): Unit = {
val t = brandField.getText.trim
laptopBrand = if (t.isEmpty) "ULTRA X9" else t
laptopFrame.setTitle(laptopBrand + " - ULTRA WINDOWS LAPTOP")
screen.repaint()
}
})
// ==========================================================
// POWER BUTTON
// ==========================================================
// Salted SHA-256 verifier for the simulated laptop login.
// The password itself is not embedded in source code.
val laptopLoginSalt = "YTR-LAPTOP-LOGIN-SALT-2026"
val laptopLoginVerifier = "2f52b3afad1d7edfe3df7406a24c03d0855be3b0ba69e8108bb839cd03944940"
def laptopSha256Hex(value: String): String = {
val digest = java.security.MessageDigest.getInstance("SHA-256")
digest.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8))
.map(b => "%02x".format(b & 0xff)).mkString
}
def requestLaptopPassword(): Boolean = {
val field = new javax.swing.JPasswordField(18)
val result = javax.swing.JOptionPane.showConfirmDialog(
laptopFrame,
Array[AnyRef](new JLabel("Enter the laptop password to unlock the desktop:"), field),
"YTR Laptop Secure Login",
javax.swing.JOptionPane.OK_CANCEL_OPTION,
javax.swing.JOptionPane.PLAIN_MESSAGE
)
val entered = new String(field.getPassword)
field.setText("")
result == javax.swing.JOptionPane.OK_OPTION &&
laptopSha256Hex(laptopLoginSalt + entered) == laptopLoginVerifier
}
powerButton.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (!powered && !requestLaptopPassword()) {
topStatus.setText("LOGIN CANCELLED OR INCORRECT PASSWORD")
screen.repaint()
return
}
powered = !powered
if (powered) {
val b = brandField.getText.trim
laptopBrand = if (b.isEmpty) "ULTRA X9" else b
laptopFrame.setTitle(laptopBrand + " - ULTRA WINDOWS LAPTOP")
powerButton.setText("POWER OFF")
brandField.setEnabled(false)
resetButton.setEnabled(true)
showDesktopIcons(true)
topStatus.setText("POWER ON | DESKTOP READY | " + laptopBrand)
} else {
powerButton.setText("POWER ON")
brandField.setEnabled(true)
showDesktopIcons(false)
startPanel.setVisible(false)
topStatus.setText("POWER OFF | Press POWER ON")
}
screen.repaint()
screen.requestFocusInWindow()
}
})
resetButton.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
powered = false
laptopBrand = "ULTRA X9"
laptopColor = new Color(48, 52, 60)
brandField.setText("ULTRA X9")
brandField.setEnabled(true)
powerButton.setText("POWER ON")
showDesktopIcons(false)
startPanel.setVisible(false)
topStatus.setText("RESET COMPLETE | POWER OFF")
screen.repaint()
}
})
// ==========================================================
// START CLICK AREA + MOUSE CURSOR MIRROR
// ==========================================================
screen.addMouseListener(new MouseAdapter {
override def mousePressed(e: MouseEvent): Unit = {
cursorX = e.getX
cursorY = e.getY
if (powered) {
val sy = screen.getHeight - 64
if (e.getY >= sy) {
if (e.getX >= 20 && e.getX <= 115) {
startOpen = !startOpen
startPanel.setVisible(startOpen)
} else if (e.getX >= 115 && e.getX <= 205) {
openCalculator()
}
}
}
screen.repaint()
}
})
screen.addMouseMotionListener(new MouseMotionAdapter {
override def mouseMoved(e: MouseEvent): Unit = {
cursorX = e.getX
cursorY = e.getY
screen.repaint()
}
override def mouseDragged(e: MouseEvent): Unit = {
cursorX = e.getX
cursorY = e.getY
screen.repaint()
}
})
// ==========================================================
// VIRTUAL KEYBOARD
// ==========================================================
def baseKeyColor(): Color = {
new Color(
math.min(255, laptopColor.getRed + 24),
math.min(255, laptopColor.getGreen + 24),
math.min(255, laptopColor.getBlue + 24)
)
}
def addKey(panel: JPanel, label: String, code: Int, width: Int): JButton = {
val b = button(label)
b.setPreferredSize(new Dimension(width, 34))
b.setBackground(baseKeyColor())
b.setForeground(Color.WHITE)
b.setFocusable(false)
b.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
b.setBackground(new Color(255, 190, 55))
topStatus.setText("KEYBOARD: " + label)
val t = new Timer(120, new ActionListener {
override def actionPerformed(e2: ActionEvent): Unit = b.setBackground(baseKeyColor())
})
t.setRepeats(false)
t.start()
screen.requestFocusInWindow()
}
})
panel.add(b)
keyMap(code) = b
b
}
def row(labels: Array[String], codes: Array[Int]): JPanel = {
val p = new JPanel(new FlowLayout(FlowLayout.CENTER, 3, 2))
p.setOpaque(false)
var ri = 0
while (ri < labels.length) {
val width =
if (labels(ri) == "SPACE") 260
else if (labels(ri).length >= 6) 78
else 45
addKey(p, labels(ri), codes(ri), width)
ri += 1
}
p
}
val keyboardPanel = new JPanel(new java.awt.GridLayout(6, 1, 4, 3))
keyboardPanel.setBackground(new Color(25, 28, 34))
keyboardPanel.add(row(
Array("ESC","F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12"),
Array(KeyEvent.VK_ESCAPE,KeyEvent.VK_F1,KeyEvent.VK_F2,KeyEvent.VK_F3,KeyEvent.VK_F4,KeyEvent.VK_F5,KeyEvent.VK_F6,KeyEvent.VK_F7,KeyEvent.VK_F8,KeyEvent.VK_F9,KeyEvent.VK_F10,KeyEvent.VK_F11,KeyEvent.VK_F12)
))
keyboardPanel.add(row(
Array("1","2","3","4","5","6","7","8","9","0","-","=","BACKSPACE"),
Array(KeyEvent.VK_1,KeyEvent.VK_2,KeyEvent.VK_3,KeyEvent.VK_4,KeyEvent.VK_5,KeyEvent.VK_6,KeyEvent.VK_7,KeyEvent.VK_8,KeyEvent.VK_9,KeyEvent.VK_0,KeyEvent.VK_MINUS,KeyEvent.VK_EQUALS,KeyEvent.VK_BACK_SPACE)
))
keyboardPanel.add(row(
Array("TAB","Q","W","E","R","T","Y","U","I","O","P","[","]","\\"),
Array(KeyEvent.VK_TAB,KeyEvent.VK_Q,KeyEvent.VK_W,KeyEvent.VK_E,KeyEvent.VK_R,KeyEvent.VK_T,KeyEvent.VK_Y,KeyEvent.VK_U,KeyEvent.VK_I,KeyEvent.VK_O,KeyEvent.VK_P,KeyEvent.VK_OPEN_BRACKET,KeyEvent.VK_CLOSE_BRACKET,KeyEvent.VK_BACK_SLASH)
))
keyboardPanel.add(row(
Array("CAPS","A","S","D","F","G","H","J","K","L",";","'","ENTER"),
Array(KeyEvent.VK_CAPS_LOCK,KeyEvent.VK_A,KeyEvent.VK_S,KeyEvent.VK_D,KeyEvent.VK_F,KeyEvent.VK_G,KeyEvent.VK_H,KeyEvent.VK_J,KeyEvent.VK_K,KeyEvent.VK_L,KeyEvent.VK_SEMICOLON,KeyEvent.VK_QUOTE,KeyEvent.VK_ENTER)
))
keyboardPanel.add(row(
Array("SHIFT","Z","X","C","V","B","N","M",",",".","/","SHIFT"),
Array(KeyEvent.VK_SHIFT,KeyEvent.VK_Z,KeyEvent.VK_X,KeyEvent.VK_C,KeyEvent.VK_V,KeyEvent.VK_B,KeyEvent.VK_N,KeyEvent.VK_M,KeyEvent.VK_COMMA,KeyEvent.VK_PERIOD,KeyEvent.VK_SLASH,KeyEvent.VK_SHIFT)
))
keyboardPanel.add(row(
Array("CTRL","ALT","SPACE","ALT","CTRL","INSERT","DELETE","HOME","END","LEFT","UP","DOWN","RIGHT"),
Array(KeyEvent.VK_CONTROL,KeyEvent.VK_ALT,KeyEvent.VK_SPACE,KeyEvent.VK_ALT,KeyEvent.VK_CONTROL,KeyEvent.VK_INSERT,KeyEvent.VK_DELETE,KeyEvent.VK_HOME,KeyEvent.VK_END,KeyEvent.VK_LEFT,KeyEvent.VK_UP,KeyEvent.VK_DOWN,KeyEvent.VK_RIGHT)
))
val keyboardDispatcher = new java.awt.KeyEventDispatcher {
override def dispatchKeyEvent(e: KeyEvent): Boolean = {
val target = keyMap.get(e.getKeyCode)
if (e.getID == KeyEvent.KEY_PRESSED) {
target.foreach { b => b.setBackground(new Color(255, 190, 55)) }
if (powered) {
topStatus.setText("REAL KEYBOARD: " + KeyEvent.getKeyText(e.getKeyCode))
}
} else if (e.getID == KeyEvent.KEY_RELEASED) {
target.foreach { b => b.setBackground(baseKeyColor()) }
}
false
}
}
java.awt.KeyboardFocusManager
.getCurrentKeyboardFocusManager
.addKeyEventDispatcher(keyboardDispatcher)
// ==========================================================
// LAPTOP BASE
// ==========================================================
val keyboardLabel = new JLabel(
"VIRTUAL KEYBOARD | REAL KEYBOARD MIRROR: ON | MOUSE CURSOR: ON",
SwingConstants.CENTER
)
keyboardLabel.setFont(new Font("Arial", Font.BOLD, 12))
keyboardLabel.setForeground(Color.WHITE)
base = new JPanel(new BorderLayout())
base.setBackground(new Color(31, 34, 41))
base.add(keyboardLabel, BorderLayout.NORTH)
base.add(keyboardPanel, BorderLayout.CENTER)
laptopFrame.add(topBar, BorderLayout.NORTH)
laptopFrame.add(screen, BorderLayout.CENTER)
laptopFrame.add(base, BorderLayout.SOUTH)
laptopFrame.addWindowListener(new java.awt.event.WindowAdapter {
override def windowClosed(e: java.awt.event.WindowEvent): Unit = {
java.awt.KeyboardFocusManager
.getCurrentKeyboardFocusManager
.removeKeyEventDispatcher(keyboardDispatcher)
}
})
laptopFrame.setVisible(true)
screen.requestFocusInWindow()
}
// ============================================================
// INTEGRATED STANDALONE APP MODULES
// ============================================================
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Dimension
import java.awt.Font
import java.awt.GridLayout
import java.awt.Image
import java.awt.Toolkit
import java.awt.datatransfer.DataFlavor
import java.awt.image.BufferedImage
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.io.PrintWriter
import java.net.Inet4Address
import java.net.InetSocketAddress
import java.net.NetworkInterface
import java.net.ServerSocket
import java.net.Socket
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Base64
import java.util.Properties
import java.util.UUID
import javax.imageio.ImageIO
import javax.swing.BorderFactory
import javax.swing.DefaultListModel
import javax.swing.JDialog
import javax.swing.JFileChooser
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.JOptionPane
import javax.swing.JPanel
import javax.swing.JPasswordField
import javax.swing.JScrollPane
import javax.swing.JTextArea
import javax.swing.JTextField
import javax.swing.JButton
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.SwingUtilities
import javax.swing.UIManager
import javax.swing.WindowConstants
import scala.collection.mutable
object LiveChatApp {
// ==========================================================
// SETTINGS
// ==========================================================
private val APP_NAME =
"YADNESH LIVE CHAT"
private val DEFAULT_PORT =
5050
// ==========================================================
// UTILITY
// ==========================================================
object Util {
private val random =
new SecureRandom()
private val encoder =
Base64.getUrlEncoder.withoutPadding()
private val decoder =
Base64.getUrlDecoder
def encode(
value: String
): String = {
val text =
if (value == null) "" else value
encoder.encodeToString(
text.getBytes(
StandardCharsets.UTF_8
)
)
}
def decode(
value: String
): String = {
if (
value == null ||
value.isEmpty
) {
""
} else {
try {
new String(
decoder.decode(value),
StandardCharsets.UTF_8
)
} catch {
case _: Throwable =>
""
}
}
}
def sha256(
value: String
): String = {
val md =
MessageDigest.getInstance(
"SHA-256"
)
md.digest(
value.getBytes(
StandardCharsets.UTF_8
)
).map(
"%02x".format(_)
).mkString
}
def randomHex(
size: Int
): String = {
val bytes =
new Array[Byte](size)
random.nextBytes(
bytes
)
bytes.map(
"%02x".format(_)
).mkString
}
def secureEquals(
a: String,
b: String
): Boolean = {
if (
a == null ||
b == null
) {
false
} else {
MessageDigest.isEqual(
a.getBytes(
StandardCharsets.UTF_8
),
b.getBytes(
StandardCharsets.UTF_8
)
)
}
}
def clean(
value: String,
maximum: Int
): String = {
val text =
if (value == null) "" else value.trim
if (
text.length <= maximum
) {
text
} else {
text.substring(
0,
maximum
)
}
}
def validUsername(
name: String
): Boolean = {
name != null &&
name.matches(
"[A-Za-z0-9_]{3,20}"
)
}
def now(): String = {
LocalDateTime.now().format(
DateTimeFormatter.ofPattern(
"HH:mm:ss"
)
)
}
// --------------------------------------------------------
// IMAGE -> BASE64
// --------------------------------------------------------
def imageToBase64(
original: Image
): String = {
if (original == null) {
return ""
}
try {
val width =
math.max(
1,
original.getWidth(null)
)
val height =
math.max(
1,
original.getHeight(null)
)
val image =
new BufferedImage(
width,
height,
BufferedImage.TYPE_INT_ARGB
)
val graphics =
image.createGraphics()
try {
graphics.drawImage(
original,
0,
0,
null
)
} finally {
graphics.dispose()
}
val out =
new ByteArrayOutputStream()
ImageIO.write(
image,
"png",
out
)
out.close()
val data =
out.toByteArray
if (
data.length >
350 * 1024
) {
""
} else {
encoder.encodeToString(
data
)
}
} catch {
case _: Throwable =>
""
}
}
// --------------------------------------------------------
// FILE -> BASE64
// --------------------------------------------------------
def fileToBase64(
path: String
): String = {
try {
val image =
ImageIO.read(
new java.io.File(
path
)
)
imageToBase64(
image
)
} catch {
case _: Throwable =>
""
}
}
// --------------------------------------------------------
// BASE64 -> IMAGE
// --------------------------------------------------------
def base64ToImage(
value: String
): BufferedImage = {
if (
value == null ||
value.isEmpty
) {
null
} else {
try {
val bytes =
decoder.decode(
value
)
ImageIO.read(
new ByteArrayInputStream(
bytes
)
)
} catch {
case _: Throwable =>
null
}
}
}
// --------------------------------------------------------
// PASTE IMAGE FROM CLIPBOARD
// --------------------------------------------------------
def clipboardImageBase64():
String = {
try {
val clipboard =
Toolkit
.getDefaultToolkit
.getSystemClipboard
if (
clipboard.isDataFlavorAvailable(
DataFlavor.imageFlavor
)
) {
val image =
clipboard.getData(
DataFlavor.imageFlavor
).asInstanceOf[Image]
imageToBase64(
image
)
} else {
""
}
} catch {
case _: Throwable =>
""
}
}
// --------------------------------------------------------
// LAN IPv4
// --------------------------------------------------------
def localIPs():
Seq[String] = {
val values =
mutable.ListBuffer[String]()
try {
val interfaces =
NetworkInterface.getNetworkInterfaces()
while (
interfaces != null &&
interfaces.hasMoreElements
) {
val network =
interfaces.nextElement()
if (
network.isUp &&
!network.isLoopback &&
!network.isVirtual
) {
val addresses =
network.getInetAddresses()
while (
addresses.hasMoreElements
) {
val address =
addresses.nextElement()
if (
address
.isInstanceOf[Inet4Address] &&
!address.isLoopbackAddress
) {
val ip =
address.getHostAddress()
if (
!values.contains(
ip
)
) {
values +=
ip
}
}
}
}
}
} catch {
case _: Throwable =>
}
if (
values.isEmpty
) {
Seq(
"127.0.0.1"
)
} else {
values.toSeq
}
}
def setupFont(): Unit = {
val font =
new Font(
"Segoe UI",
Font.PLAIN,
14
)
UIManager.put(
"Label.font",
font
)
UIManager.put(
"Button.font",
font
)
UIManager.put(
"TextField.font",
font
)
UIManager.put(
"PasswordField.font",
font
)
UIManager.put(
"TextArea.font",
font
)
UIManager.put(
"List.font",
font
)
}
}
// ==========================================================
// DATA
// ==========================================================
case class UserRecord(
username: String,
passwordSalt: String,
passwordVerifier: String,
bio: String,
avatar: String,
friends: Set[String] = Set.empty[String],
incomingRequests: Set[String] = Set.empty[String],
blocked: Set[String] = Set.empty[String]
)
case class ChatMessage(
sender: String,
text: String,
time: String
)
case class PersonItem(
username: String,
bio: String,
online: Boolean,
relation: String = "NONE"
) {
override def toString: String = {
val state =
if (online) "? ONLINE" else "? OFFLINE"
val rel =
relation match {
case "FRIEND" => " ? FRIEND"
case "INCOMING" => " ? REQUEST"
case "OUTGOING" => " ? REQUEST SENT"
case "BLOCKED" => " ? BLOCKED"
case _ => ""
}
username +
" " +
state +
rel
}
}
case class ChatItem(
id: String,
title: String,
group: Boolean,
unread: Int = 0
) {
override def toString: String = {
if (unread > 0) {
title + " (" + unread + " new)"
} else {
title
}
}
}
// ==========================================================
// AUTOMATIC LAN DISCOVERY
// ==========================================================
object LanDiscovery {
private val DISCOVERY_PORT =
5051
private val MAGIC =
"YADNESH_LIVE_CHAT_SERVER|"
@volatile private var beaconRunning =
false
private var beaconThread:
Thread = null
def startBeacon(
tcpPort: Int
): Unit = {
if (beaconRunning) {
return
}
beaconRunning = true
beaconThread =
new Thread(
new Runnable {
override def run(): Unit = {
var socket:
DatagramSocket = null
try {
socket =
new DatagramSocket()
socket.setBroadcast(
true
)
while (
beaconRunning
) {
val bytes =
(
MAGIC +
tcpPort
).getBytes(
StandardCharsets.UTF_8
)
val packet =
new DatagramPacket(
bytes,
bytes.length,
java.net.InetAddress
.getByName(
"255.255.255.255"
),
DISCOVERY_PORT
)
try {
socket.send(packet)
} catch {
case _: Throwable =>
}
try {
Thread.sleep(
1000L
)
} catch {
case _: InterruptedException =>
beaconRunning = false
}
}
} catch {
case _: Throwable =>
} finally {
if (
socket != null
) {
try {
socket.close()
} catch {
case _: Throwable =>
}
}
}
}
},
"Yadnesh-Chat-Beacon"
)
beaconThread.setDaemon(
true
)
beaconThread.start()
}
def stopBeacon(): Unit = {
beaconRunning = false
if (
beaconThread != null
) {
try {
beaconThread.interrupt()
} catch {
case _: Throwable =>
}
}
beaconThread = null
}
def findServer(
timeoutMs: Int
): Option[(String, Int)] = {
var socket:
DatagramSocket = null
try {
socket =
new DatagramSocket(
DISCOVERY_PORT
)
socket.setSoTimeout(
math.max(
100,
timeoutMs
)
)
val buffer =
new Array[Byte](
512
)
val packet =
new DatagramPacket(
buffer,
buffer.length
)
socket.receive(
packet
)
val text =
new String(
packet.getData,
packet.getOffset,
packet.getLength,
StandardCharsets.UTF_8
).trim
if (
text.startsWith(
MAGIC
)
) {
val portText =
text.substring(
MAGIC.length
)
try {
val port =
portText.toInt
if (
port >= 1024 &&
port <= 65535
) {
Some(
(
packet
.getAddress
.getHostAddress,
port
)
)
} else {
None
}
} catch {
case _: Throwable =>
None
}
} else {
None
}
} catch {
case _: java.net.SocketTimeoutException =>
None
case _: Throwable =>
None
} finally {
if (
socket != null
) {
try {
socket.close()
} catch {
case _: Throwable =>
}
}
}
}
}
// ==========================================================
// SERVER
// ==========================================================
object Server {
private val lock =
new Object
private val users =
mutable.Map[
String,
UserRecord
]()
private val sessions =
mutable.Map[
String,
ClientConnection
]()
private val chatMembers =
mutable.Map[
String,
mutable.Set[String]
]()
private val chatNames =
mutable.Map[
String,
String
]()
private val chatGroups =
mutable.Map[
String,
Boolean
]()
private val chatHistory =
mutable.Map[
String,
mutable.ListBuffer[
ChatMessage
]
]()
private val loginChallenges =
mutable.Map[
Socket,
(String, String)
]()
private val registerChallenges =
mutable.Map[
Socket,
(String, String)
]()
private val recoveryChallenges =
mutable.Map[
Socket,
(String, String)
]()
// --------------------------------------------------------
// RECOVERY SECRET HASH
// --------------------------------------------------------
//
// This is the SHA-256 verifier of the private recovery
// secret. The secret itself is not shown in the UI/code.
//
// --------------------------------------------------------
private val recoveryVerifier =
"7f3f667469f4945d7b63b6831e26f1bd84bd02d57b455542b6144fe2b2a6a92d"
private val dataFolder =
Paths.get(
"YadneshLiveChatData"
)
private val profilesFolder =
dataFolder.resolve(
"profiles"
)
private var serverSocket:
ServerSocket = null
private var running =
false
// ========================================================
// CONNECTION
// ========================================================
class ClientConnection(
val socket: Socket,
val reader: BufferedReader,
val writer: PrintWriter
) {
var username =
""
def send(
line: String
): Unit = {
lock.synchronized {
try {
if (
!socket.isClosed
) {
writer.println(
line
)
writer.flush()
}
} catch {
case _: Throwable =>
}
}
}
def close(): Unit = {
try {
socket.close()
} catch {
case _: Throwable =>
}
}
}
// ========================================================
// START SERVER
// ========================================================
def start(
port: Int
): Boolean = {
lock.synchronized {
if (running) {
true
} else {
try {
Files.createDirectories(
profilesFolder
)
loadProfiles()
serverSocket =
new ServerSocket()
serverSocket.setReuseAddress(
true
)
// Important:
// listen on ALL network interfaces.
serverSocket.bind(
new InetSocketAddress(
"0.0.0.0",
port
)
)
running =
true
val thread =
new Thread(
new Runnable {
override def run(): Unit = {
acceptLoop()
}
}
)
thread.setDaemon(
true
)
thread.start()
LanDiscovery.startBeacon(
port
)
true
} catch {
case _: Throwable =>
running =
false
false
}
}
}
}
// ========================================================
// ACCEPT LOOP
// ========================================================
private def acceptLoop():
Unit = {
while (
running
) {
try {
val sock =
serverSocket.accept()
sock.setKeepAlive(
true
)
sock.setTcpNoDelay(
true
)
val reader =
new BufferedReader(
new InputStreamReader(
sock.getInputStream,
StandardCharsets.UTF_8
)
)
val writer =
new PrintWriter(
new OutputStreamWriter(
sock.getOutputStream,
StandardCharsets.UTF_8
),
true
)
val client =
new ClientConnection(
sock,
reader,
writer
)
val thread =
new Thread(
new Runnable {
override def run(): Unit = {
clientLoop(
client
)
}
}
)
thread.setDaemon(
true
)
thread.start()
} catch {
case _: Throwable =>
}
}
}
// ========================================================
// PROFILE FILE
// ========================================================
private def profileFile(
username: String
): Path = {
profilesFolder.resolve(
username +
".profile"
)
}
private def saveProfile(
user: UserRecord
): Unit = {
val props =
new Properties()
props.setProperty(
"username",
user.username
)
props.setProperty(
"passwordSalt",
user.passwordSalt
)
props.setProperty(
"passwordVerifier",
user.passwordVerifier
)
props.setProperty(
"bio",
user.bio
)
props.setProperty(
"avatar",
user.avatar
)
props.setProperty(
"friends",
user.friends.toSeq.sorted.mkString(",")
)
props.setProperty(
"incomingRequests",
user.incomingRequests.toSeq.sorted.mkString(",")
)
props.setProperty(
"blocked",
user.blocked.toSeq.sorted.mkString(",")
)
val out =
Files.newOutputStream(
profileFile(
user.username
)
)
try {
props.store(
out,
"Yadnesh Live Chat Account"
)
} finally {
out.close()
}
}
// ========================================================
// LOAD ALL ACCOUNTS
// ========================================================
private def loadProfiles():
Unit = {
users.clear()
if (
!Files.exists(
profilesFolder
)
) {
return
}
val stream =
Files.list(
profilesFolder
)
try {
val iterator =
stream.iterator()
while (
iterator.hasNext
) {
val path =
iterator.next()
if (
path.toString
.endsWith(
".profile"
)
) {
try {
val props =
new Properties()
val input =
Files.newInputStream(
path
)
try {
props.load(
input
)
} finally {
input.close()
}
val username =
props.getProperty(
"username",
""
)
if (
Util.validUsername(
username
)
) {
val friends =
props
.getProperty(
"friends",
""
)
.split(",")
.map(_.trim)
.filter(_.nonEmpty)
.toSet
val incomingRequests =
props
.getProperty(
"incomingRequests",
""
)
.split(",")
.map(_.trim)
.filter(_.nonEmpty)
.toSet
val blocked =
props
.getProperty(
"blocked",
""
)
.split(",")
.map(_.trim)
.filter(_.nonEmpty)
.toSet
val user =
UserRecord(
username,
props.getProperty(
"passwordSalt",
""
),
props.getProperty(
"passwordVerifier",
""
),
props.getProperty(
"bio",
""
),
props.getProperty(
"avatar",
""
),
friends,
incomingRequests,
blocked
)
users.put(
username.toLowerCase,
user
)
}
} catch {
case _: Throwable =>
}
}
}
} finally {
stream.close()
}
}
// ========================================================
// FRIENDSHIP HELPERS
// ========================================================
private def canonicalUser(
username: String
): Option[String] = {
users.get(
username.toLowerCase
).map(_.username)
}
private def isFriend(
a: String,
b: String
): Boolean = {
users
.get(a.toLowerCase)
.exists(_.friends.exists(_.equalsIgnoreCase(b)))
}
private def isBlocked(
a: String,
b: String
): Boolean = {
users
.get(a.toLowerCase)
.exists(_.blocked.exists(_.equalsIgnoreCase(b)))
}
private def isMutuallyBlocked(
a: String,
b: String
): Boolean = {
isBlocked(a, b) || isBlocked(b, a)
}
private def removeFriendshipBetween(
a: String,
b: String
): Unit = {
users.get(a.toLowerCase).foreach { ua =>
val updated = ua.copy(
friends = ua.friends.filterNot(_.equalsIgnoreCase(b)),
incomingRequests = ua.incomingRequests.filterNot(_.equalsIgnoreCase(b))
)
users.put(ua.username.toLowerCase, updated)
saveProfile(updated)
}
users.get(b.toLowerCase).foreach { ub =>
val updated = ub.copy(
friends = ub.friends.filterNot(_.equalsIgnoreCase(a)),
incomingRequests = ub.incomingRequests.filterNot(_.equalsIgnoreCase(a))
)
users.put(ub.username.toLowerCase, updated)
saveProfile(updated)
}
}
private def relationBetween(
viewer: String,
target: String
): String = {
users.get(target.toLowerCase) match {
case None =>
"NONE"
case Some(targetUser) =>
if (
isMutuallyBlocked(viewer, targetUser.username)
) {
"BLOCKED"
} else if (
users
.get(viewer.toLowerCase)
.exists(_.friends.exists(_.equalsIgnoreCase(targetUser.username)))
) {
"FRIEND"
} else if (
targetUser.incomingRequests.exists(_.equalsIgnoreCase(viewer))
) {
"OUTGOING"
} else if (
users
.get(viewer.toLowerCase)
.exists(_.incomingRequests.exists(_.equalsIgnoreCase(targetUser.username)))
) {
"INCOMING"
} else {
"NONE"
}
}
}
private def ensureDirectChat(
a: String,
b: String
): String = {
val chatId =
directChatId(
a,
b
)
addMember(
chatId,
a
)
addMember(
chatId,
b
)
chatGroups.put(
chatId,
false
)
chatNames.put(
chatId,
""
)
chatHistory.getOrElseUpdate(
chatId,
mutable.ListBuffer[ChatMessage]()
)
chatId
}
private def sendFriendsList(
username: String
): Unit = {
users
.get(username.toLowerCase)
.foreach { user =>
val friends =
user.friends
.flatMap(name => users.get(name.toLowerCase).map(_.username))
.toSeq
.sortBy(_.toLowerCase)
val out =
new StringBuilder(
"FRIENDS_LIST|" + friends.length
)
friends.foreach { name =>
val friendUser = users(name.toLowerCase)
out.append("|").append(Util.encode(friendUser.username))
out.append("|").append(
if (sessions.contains(friendUser.username.toLowerCase)) "1" else "0"
)
out.append("|").append(Util.encode(friendUser.bio))
}
sendToUser(
username,
out.toString
)
}
}
private def sendFriendRequestsList(
username: String
): Unit = {
users
.get(username.toLowerCase)
.foreach { user =>
val requests =
user.incomingRequests
.flatMap(name => users.get(name.toLowerCase).map(_.username))
.toSeq
.sortBy(_.toLowerCase)
val out =
new StringBuilder(
"FRIEND_REQUESTS|" + requests.length
)
requests.foreach { name =>
val requester = users(name.toLowerCase)
out.append("|").append(Util.encode(requester.username))
out.append("|").append(
if (sessions.contains(requester.username.toLowerCase)) "1" else "0"
)
out.append("|").append(Util.encode(requester.bio))
}
sendToUser(
username,
out.toString
)
}
}
// ========================================================
// DIRECT CHAT ID
// ========================================================
private def directChatId(
a: String,
b: String
): String = {
val pair =
Seq(
a.toLowerCase,
b.toLowerCase
).sorted
"DM:" +
pair.head +
":" +
pair(1)
}
// ========================================================
// ADD MEMBER
// ========================================================
private def addMember(
chatId: String,
username: String
): Unit = {
val set =
chatMembers.getOrElseUpdate(
chatId,
mutable.Set[String]()
)
set +=
username
}
// ========================================================
// SEND TO USER
// ========================================================
private def sendToUser(
username: String,
line: String
): Unit = {
sessions
.get(
username.toLowerCase
)
.foreach {
client =>
client.send(
line
)
}
}
// ========================================================
// BROADCAST CHAT
// ========================================================
private def broadcastChat(
chatId: String,
line: String
): Unit = {
chatMembers
.get(chatId)
.foreach {
members =>
members.foreach {
username =>
sendToUser(
username,
line
)
}
}
}
// ========================================================
// SEND CHAT TO USER
// ========================================================
private def addChatToUser(
chatId: String,
username: String
): Unit = {
val group =
chatGroups.getOrElse(
chatId,
false
)
val title = {
if (group) {
"#" +
chatNames.getOrElse(
chatId,
"Group"
)
} else {
val members =
chatMembers.getOrElse(
chatId,
mutable.Set[String]()
)
val other =
members.find {
x =>
!x.equalsIgnoreCase(
username
)
}
other match {
case Some(value) =>
"@" + value
case None =>
"@Chat"
}
}
}
sendToUser(
username,
"CHAT|" +
Util.encode(
chatId
) +
"|" +
Util.encode(
title
) +
"|" +
(
if (group) "1"
else "0"
)
)
}
// ========================================================
// SEND ALL USER CHATS
// ========================================================
private def sendAllUserChats(
username: String
): Unit = {
users
.get(username.toLowerCase)
.foreach { user =>
user.friends.foreach { friendName =>
users.get(friendName.toLowerCase).foreach { friend =>
val chatId = ensureDirectChat(user.username, friend.username)
addChatToUser(chatId, user.username)
}
}
}
chatMembers.keys.foreach {
chatId =>
chatMembers
.get(chatId)
.foreach { members =>
if (
members.contains(
username
)
) {
addChatToUser(
chatId,
username
)
}
}
}
}
// ========================================================
// HISTORY
// ========================================================
private def sendHistory(
client: ClientConnection,
chatId: String
): Unit = {
val list =
chatHistory.getOrElse(
chatId,
mutable.ListBuffer[
ChatMessage
]()
)
val start =
math.max(
0,
list.length - 100
)
val recent =
list.slice(
start,
list.length
)
val out =
new StringBuilder()
out.append(
"HISTORY|"
)
out.append(
Util.encode(
chatId
)
)
out.append(
"|" +
recent.length
)
recent.foreach {
message =>
out.append(
"|" +
Util.encode(
message.sender
)
)
out.append(
"|" +
Util.encode(
message.time
)
)
out.append(
"|" +
Util.encode(
message.text
)
)
}
client.send(
out.toString
)
}
// ========================================================
// CLIENT LOOP
// ========================================================
private def clientLoop(
client: ClientConnection
): Unit = {
try {
while (
!client.socket.isClosed
) {
val line =
client.reader.readLine()
if (
line == null
) {
throw new IOException(
"Disconnected"
)
}
processCommand(
client,
line
)
}
} catch {
case _: Throwable =>
}
lock.synchronized {
val name =
client.username
if (
name != null &&
name.nonEmpty
) {
sessions.get(
name.toLowerCase
) match {
case Some(current)
if current eq client =>
sessions.remove(
name.toLowerCase
)
broadcastPresence(
name
)
case _ =>
}
}
loginChallenges.remove(
client.socket
)
registerChallenges.remove(
client.socket
)
recoveryChallenges.remove(
client.socket
)
}
client.close()
}
// ========================================================
// COMMAND PROCESSOR
// ========================================================
private def processCommand(
client: ClientConnection,
line: String
): Unit = {
val p =
line.split(
"\\|",
-1
)
if (
p.length == 0
) {
return
}
val command =
p(0)
// ======================================================
// REGISTER START
// ======================================================
if (
command ==
"REGISTER_START"
) {
if (
p.length < 2
) {
return
}
val username =
Util.decode(
p(1)
).trim
if (
!Util.validUsername(
username
)
) {
client.send(
"ERROR|" +
Util.encode(
"Username must be 3-20 characters."
)
)
return
}
lock.synchronized {
if (
users.contains(
username.toLowerCase
)
) {
client.send(
"ERROR|" +
Util.encode(
"This account already exists. Login instead."
)
)
} else {
val salt =
Util.randomHex(
16
)
registerChallenges.put(
client.socket,
(
username,
salt
)
)
client.send(
"REGISTER_CHALLENGE|" +
Util.encode(
salt
)
)
}
}
return
}
// ======================================================
// REGISTER FINISH
// ======================================================
if (
command ==
"REGISTER_FINISH"
) {
if (
p.length < 6
) {
return
}
lock.synchronized {
registerChallenges
.get(
client.socket
) match {
case None =>
client.send(
"ERROR|" +
Util.encode(
"Registration expired."
)
)
case Some(
(
username,
salt
)
) =>
if (
users.contains(
username.toLowerCase
)
) {
client.send(
"ERROR|" +
Util.encode(
"Account already exists."
)
)
registerChallenges.remove(
client.socket
)
} else {
val passwordVerifier =
Util.decode(
p(2)
)
val bio =
Util.clean(
Util.decode(
p(3)
),
500
)
val avatar =
Util.decode(
p(4)
)
val validAvatar =
if (
avatar.length <=
480000
) {
avatar
} else {
""
}
val user =
UserRecord(
username,
salt,
passwordVerifier,
bio,
validAvatar
)
users.put(
username.toLowerCase,
user
)
saveProfile(
user
)
registerChallenges.remove(
client.socket
)
client.username =
username
sessions.put(
username.toLowerCase,
client
)
client.send(
"LOGIN_OK|" +
Util.encode(
username
)
)
sendAllUserChats(
username
)
sendFriendsList(
username
)
sendFriendRequestsList(
username
)
broadcastPresence(
username
)
}
}
}
return
}
// ======================================================
// LOGIN START
// ======================================================
if (
command ==
"LOGIN_START"
) {
if (
p.length < 2
) {
return
}
val username =
Util.decode(
p(1)
).trim
lock.synchronized {
users.get(
username.toLowerCase
) match {
case None =>
client.send(
"ERROR|" +
Util.encode(
"Account not found. Create a new account."
)
)
case Some(user) =>
val nonce =
Util.randomHex(
32
)
loginChallenges.put(
client.socket,
(
user.username,
nonce
)
)
client.send(
"LOGIN_CHALLENGE|" +
Util.encode(
user.passwordSalt
) +
"|" +
Util.encode(
nonce
)
)
}
}
return
}
// ======================================================
// LOGIN FINISH
// ======================================================
if (
command ==
"LOGIN_FINISH"
) {
if (
p.length < 3
) {
return
}
lock.synchronized {
loginChallenges
.get(
client.socket
) match {
case None =>
client.send(
"ERROR|" +
Util.encode(
"Login expired."
)
)
case Some(
(
username,
nonce
)
) =>
users.get(
username.toLowerCase
) match {
case None =>
client.send(
"ERROR|" +
Util.encode(
"Account not found."
)
)
case Some(user) =>
val proof =
Util.decode(
p(2)
)
val expected =
Util.sha256(
user.passwordVerifier +
nonce
)
if (
Util.secureEquals(
proof,
expected
)
) {
sessions
.get(
username.toLowerCase
)
.foreach {
old =>
old.close()
}
client.username =
username
sessions.put(
username.toLowerCase,
client
)
loginChallenges.remove(
client.socket
)
client.send(
"LOGIN_OK|" +
Util.encode(
username
)
)
sendAllUserChats(
username
)
sendFriendsList(
username
)
sendFriendRequestsList(
username
)
broadcastPresence(
username
)
} else {
loginChallenges.remove(
client.socket
)
client.send(
"ERROR|" +
Util.encode(
"Wrong password."
)
)
}
}
}
}
return
}
// ======================================================
// FORGOT PASSWORD START
// ======================================================
if (
command ==
"RECOVERY_START"
) {
if (
p.length < 2
) {
return
}
val username =
Util.decode(
p(1)
).trim
lock.synchronized {
users.get(
username.toLowerCase
) match {
case None =>
client.send(
"ERROR|" +
Util.encode(
"Recovery failed."
)
)
case Some(user) =>
val nonce =
Util.randomHex(
32
)
recoveryChallenges.put(
client.socket,
(
user.username,
nonce
)
)
client.send(
"RECOVERY_CHALLENGE|" +
Util.encode(
nonce
)
)
}
}
return
}
// ======================================================
// FORGOT PASSWORD FINISH
// ======================================================
if (
command ==
"RECOVERY_FINISH"
) {
if (
p.length < 5
) {
return
}
lock.synchronized {
recoveryChallenges
.get(
client.socket
) match {
case None =>
client.send(
"ERROR|" +
Util.encode(
"Recovery expired."
)
)
case Some(
(
username,
nonce
)
) =>
val proof =
Util.decode(
p(2)
)
val newSalt =
Util.decode(
p(3)
)
val newVerifier =
Util.decode(
p(4)
)
val expected =
Util.sha256(
recoveryVerifier +
nonce
)
if (
Util.secureEquals(
proof,
expected
)
) {
users.get(
username.toLowerCase
) match {
case Some(oldUser) =>
val updated =
oldUser.copy(
passwordSalt =
newSalt,
passwordVerifier =
newVerifier
)
users.put(
username.toLowerCase,
updated
)
saveProfile(
updated
)
client.send(
"RECOVERY_OK"
)
case None =>
client.send(
"ERROR|" +
Util.encode(
"Recovery failed."
)
)
}
} else {
client.send(
"ERROR|" +
Util.encode(
"Recovery failed."
)
)
}
recoveryChallenges.remove(
client.socket
)
}
}
return
}
// ======================================================
// REQUIRE LOGIN
// ======================================================
val currentUser =
client.username
if (
currentUser == null ||
currentUser.isEmpty
) {
client.send(
"ERROR|" +
Util.encode(
"Please login first."
)
)
return
}
// ======================================================
// SEARCH PEOPLE
// ======================================================
if (
command ==
"SEARCH"
) {
val query =
if (
p.length >= 2
) {
Util.decode(
p(1)
).trim.toLowerCase
} else {
""
}
val found =
users.values
.filter {
user =>
user.username
.toLowerCase
.contains(
query
) &&
!user.username
.equalsIgnoreCase(
currentUser
)
}
.toSeq
.sortBy(
_.username.toLowerCase
)
.take(100)
val result =
new StringBuilder()
result.append(
"SEARCH_RESULT|" +
found.length
)
found.foreach {
user =>
result.append(
"|" +
Util.encode(
user.username
)
)
result.append(
"|" +
Util.encode(
user.bio
)
)
result.append(
"|" +
(
if (
sessions.contains(
user.username.toLowerCase
)
) {
"1"
} else {
"0"
}
)
)
result.append(
"|" +
relationBetween(
currentUser,
user.username
)
)
}
client.send(
result.toString
)
return
}
// ======================================================
// GET PROFILE
// ======================================================
if (
command ==
"PROFILE"
) {
if (
p.length < 2
) {
return
}
val target =
Util.decode(
p(1)
).trim
users.get(
target.toLowerCase
) match {
case None =>
client.send(
"ERROR|" +
Util.encode(
"Profile not found."
)
)
case Some(user) =>
client.send(
"PROFILE_DATA|" +
Util.encode(
user.username
) +
"|" +
Util.encode(
user.bio
) +
"|" +
Util.encode(
user.avatar
) +
"|" +
(
if (
sessions.contains(
user.username.toLowerCase
)
) {
"1"
} else {
"0"
}
) +
"|" +
relationBetween(
currentUser,
user.username
)
)
}
return
}
// ======================================================
// UPDATE PROFILE
// ======================================================
if (
command ==
"UPDATE_PROFILE"
) {
if (
p.length < 3
) {
return
}
lock.synchronized {
users.get(
currentUser.toLowerCase
) match {
case None =>
client.send(
"ERROR|" +
Util.encode(
"Profile not found."
)
)
case Some(oldUser) =>
val bio =
Util.clean(
Util.decode(
p(1)
),
500
)
val avatar =
Util.decode(
p(2)
)
val finalAvatar =
if (
avatar.length <=
480000
) {
avatar
} else {
oldUser.avatar
}
val updated =
oldUser.copy(
bio =
bio,
avatar =
finalAvatar
)
users.put(
currentUser.toLowerCase,
updated
)
saveProfile(
updated
)
client.send(
"PROFILE_SAVED"
)
onlineUsersNotify(
currentUser
)
}
}
return
}
// ======================================================
// OPEN DIRECT CHAT
// ======================================================
if (
command ==
"OPEN_DM"
) {
if (
p.length < 2
) {
return
}
val targetName =
Util.decode(
p(1)
).trim
users.get(
targetName.toLowerCase
) match {
case None =>
client.send(
"ERROR|" +
Util.encode(
"User does not exist."
)
)
case Some(targetUser) =>
if (
targetUser.username.equalsIgnoreCase(
currentUser
)
) {
client.send(
"ERROR|" +
Util.encode(
"You cannot chat with yourself."
)
)
} else if (
isMutuallyBlocked(
currentUser,
targetUser.username
)
) {
client.send(
"ERROR|" +
Util.encode(
"You cannot open this chat because one of the users is blocked."
)
)
} else if (
!isFriend(
currentUser,
targetUser.username
)
) {
client.send(
"ERROR|" +
Util.encode(
"Become friends first: send and accept a friend request."
)
)
} else {
lock.synchronized {
val chatId =
ensureDirectChat(
currentUser,
targetUser.username
)
addChatToUser(
chatId,
currentUser
)
addChatToUser(
chatId,
targetUser.username
)
sendHistory(
client,
chatId
)
}
}
}
return
}
// ======================================================
// TYPING INDICATOR
// ======================================================
if (
command ==
"TYPING"
) {
if (p.length < 3) return
val chatId = Util.decode(p(1))
val state = if (p(2) == "1") "1" else "0"
lock.synchronized {
val allowed = chatMembers.get(chatId).exists(_.contains(currentUser))
if (allowed) {
broadcastChat(
chatId,
"TYPING|" +
Util.encode(chatId) +
"|" +
Util.encode(currentUser) +
"|" +
state
)
}
}
return
}
// ======================================================
// BLOCK / UNBLOCK
// ======================================================
if (command == "BLOCK_USER" || command == "UNBLOCK_USER") {
if (p.length < 2) return
val targetName = Util.decode(p(1)).trim
lock.synchronized {
users.get(targetName.toLowerCase) match {
case None =>
client.send("ERROR|" + Util.encode("User does not exist."))
case Some(targetUser) =>
if (targetUser.username.equalsIgnoreCase(currentUser)) {
client.send("ERROR|" + Util.encode("You cannot block yourself."))
} else {
users.get(currentUser.toLowerCase).foreach { me =>
if (command == "BLOCK_USER") {
val updatedMe = me.copy(
blocked = me.blocked + targetUser.username,
friends = me.friends.filterNot(_.equalsIgnoreCase(targetUser.username)),
incomingRequests = me.incomingRequests.filterNot(_.equalsIgnoreCase(targetUser.username))
)
users.put(me.username.toLowerCase, updatedMe)
saveProfile(updatedMe)
val updatedTarget = targetUser.copy(
friends = targetUser.friends.filterNot(_.equalsIgnoreCase(me.username)),
incomingRequests = targetUser.incomingRequests.filterNot(_.equalsIgnoreCase(me.username))
)
users.put(targetUser.username.toLowerCase, updatedTarget)
saveProfile(updatedTarget)
client.send("BLOCK_STATE|" + Util.encode(targetUser.username) + "|1")
sendToUser(targetUser.username, "BLOCKED_BY|" + Util.encode(me.username))
sendFriendsList(me.username)
sendFriendsList(targetUser.username)
sendFriendRequestsList(me.username)
sendFriendRequestsList(targetUser.username)
searchNotifyAll()
} else {
val updatedMe = me.copy(
blocked = me.blocked.filterNot(_.equalsIgnoreCase(targetUser.username))
)
users.put(me.username.toLowerCase, updatedMe)
saveProfile(updatedMe)
client.send("BLOCK_STATE|" + Util.encode(targetUser.username) + "|0")
sendToUser(targetUser.username, "UNBLOCKED_BY|" + Util.encode(me.username))
searchNotifyAll()
}
}
}
}
}
return
}
// ======================================================
// FRIEND REQUEST
// ======================================================
if (
command ==
"FRIEND_REQUEST"
) {
if (p.length < 2) return
val targetName = Util.decode(p(1)).trim
lock.synchronized {
users.get(targetName.toLowerCase) match {
case None =>
client.send("ERROR|" + Util.encode("User does not exist."))
case Some(targetUser) =>
if (targetUser.username.equalsIgnoreCase(currentUser)) {
client.send("ERROR|" + Util.encode("You cannot add yourself."))
} else if (isMutuallyBlocked(currentUser, targetUser.username)) {
client.send("ERROR|" + Util.encode("Friend request cannot be sent because one of the users is blocked."))
} else if (isFriend(currentUser, targetUser.username)) {
client.send("ERROR|" + Util.encode("You are already friends."))
} else if (targetUser.incomingRequests.exists(_.equalsIgnoreCase(currentUser))) {
client.send("ERROR|" + Util.encode("Friend request already sent."))
} else if (users.get(currentUser.toLowerCase).exists(_.incomingRequests.exists(_.equalsIgnoreCase(targetUser.username)))) {
client.send("ERROR|" + Util.encode("This person already sent you a request. Accept it instead."))
} else {
val updatedTarget = targetUser.copy(
incomingRequests = targetUser.incomingRequests + currentUser
)
users.put(targetUser.username.toLowerCase, updatedTarget)
saveProfile(updatedTarget)
client.send(
"FRIEND_REQUEST_SENT|" + Util.encode(targetUser.username)
)
sendToUser(
targetUser.username,
"FRIEND_REQUEST_RECEIVED|" + Util.encode(currentUser)
)
}
}
}
return
}
// ======================================================
// FRIEND ACCEPT
// ======================================================
if (
command ==
"FRIEND_ACCEPT"
) {
if (p.length < 2) return
val requesterName = Util.decode(p(1)).trim
lock.synchronized {
users.get(requesterName.toLowerCase) match {
case None =>
client.send("ERROR|" + Util.encode("User does not exist."))
case Some(requester) =>
users.get(currentUser.toLowerCase) match {
case None =>
client.send("ERROR|" + Util.encode("Current account not found."))
case Some(me) =>
val requestExists = me.incomingRequests.exists(_.equalsIgnoreCase(requester.username))
if (!requestExists) {
client.send("ERROR|" + Util.encode("No pending friend request from this person."))
} else {
val updatedMe = me.copy(
friends = me.friends + requester.username,
incomingRequests = me.incomingRequests.filterNot(_.equalsIgnoreCase(requester.username))
)
val updatedRequester = requester.copy(
friends = requester.friends + me.username
)
users.put(me.username.toLowerCase, updatedMe)
users.put(requester.username.toLowerCase, updatedRequester)
saveProfile(updatedMe)
saveProfile(updatedRequester)
val chatId = ensureDirectChat(me.username, requester.username)
addChatToUser(chatId, me.username)
addChatToUser(chatId, requester.username)
client.send(
"FRIEND_ACCEPTED|" + Util.encode(requester.username)
)
sendToUser(
requester.username,
"FRIEND_ACCEPTED|" + Util.encode(me.username)
)
sendFriendsList(me.username)
sendFriendRequestsList(me.username)
sendFriendsList(requester.username)
sendFriendRequestsList(requester.username)
}
}
}
}
return
}
// ======================================================
// FRIEND REJECT
// ======================================================
if (
command ==
"FRIEND_REJECT"
) {
if (p.length < 2) return
val requesterName = Util.decode(p(1)).trim
lock.synchronized {
users.get(currentUser.toLowerCase) match {
case Some(me) =>
val filtered = me.incomingRequests.filterNot(_.equalsIgnoreCase(requesterName))
val updated = me.copy(incomingRequests = filtered)
users.put(me.username.toLowerCase, updated)
saveProfile(updated)
sendFriendRequestsList(me.username)
sendToUser(
requesterName,
"FRIEND_REQUEST_REJECTED|" + Util.encode(me.username)
)
case None =>
}
}
return
}
// ======================================================
// REMOVE FRIEND
// ======================================================
if (
command ==
"FRIEND_REMOVE"
) {
if (p.length < 2) return
val targetName = Util.decode(p(1)).trim
lock.synchronized {
users.get(targetName.toLowerCase) match {
case Some(targetUser) =>
users.get(currentUser.toLowerCase) match {
case Some(me) =>
val updatedMe = me.copy(
friends = me.friends.filterNot(_.equalsIgnoreCase(targetUser.username))
)
val updatedTarget = targetUser.copy(
friends = targetUser.friends.filterNot(_.equalsIgnoreCase(me.username))
)
users.put(me.username.toLowerCase, updatedMe)
users.put(targetUser.username.toLowerCase, updatedTarget)
saveProfile(updatedMe)
saveProfile(updatedTarget)
client.send("FRIEND_REMOVED|" + Util.encode(targetUser.username))
sendToUser(targetUser.username, "FRIEND_REMOVED|" + Util.encode(me.username))
sendFriendsList(me.username)
sendFriendsList(targetUser.username)
case None =>
}
case None =>
client.send("ERROR|" + Util.encode("User does not exist."))
}
}
return
}
// ======================================================
// FRIENDS LIST
// ======================================================
if (
command ==
"FRIENDS_LIST"
) {
lock.synchronized {
sendFriendsList(currentUser)
}
return
}
// ======================================================
// FRIEND REQUESTS LIST
// ======================================================
if (
command ==
"FRIEND_REQUESTS"
) {
lock.synchronized {
sendFriendRequestsList(currentUser)
}
return
}
// ======================================================
// CREATE GROUP
// ======================================================
if (
command ==
"CREATE_GROUP"
) {
if (
p.length < 3
) {
return
}
val groupName =
Util.clean(
Util.decode(
p(1)
),
40
)
val count =
try {
p(2).toInt
} catch {
case _: Throwable =>
0
}
if (
groupName.isEmpty ||
count < 1 ||
count > 20 ||
p.length <
3 + count
) {
client.send(
"ERROR|" +
Util.encode(
"Invalid group."
)
)
return
}
lock.synchronized {
val members =
mutable.Set[String]()
members +=
currentUser
var i =
0
while (
i < count
) {
val name =
Util.decode(
p(
3 + i
)
).trim
if (
users.contains(
name.toLowerCase
) &&
!name.equalsIgnoreCase(
currentUser
)
) {
members +=
users(
name.toLowerCase
).username
}
i +=
1
}
if (
members.size < 2
) {
client.send(
"ERROR|" +
Util.encode(
"At least one other person is required."
)
)
} else {
val chatId =
"GROUP:" +
UUID.randomUUID()
.toString
chatMembers.put(
chatId,
members
)
chatGroups.put(
chatId,
true
)
chatNames.put(
chatId,
groupName
)
chatHistory.put(
chatId,
mutable.ListBuffer[
ChatMessage
]()
)
members.foreach {
member =>
addChatToUser(
chatId,
member
)
}
client.send(
"GROUP_CREATED|" +
Util.encode(
groupName
)
)
}
}
return
}
// ======================================================
// OPEN CHAT
// ======================================================
if (
command ==
"OPEN_CHAT"
) {
if (
p.length < 2
) {
return
}
val chatId =
Util.decode(
p(1)
)
val allowed =
chatMembers
.get(chatId)
.exists {
members =>
members.contains(
currentUser
)
}
if (
allowed
) {
sendHistory(
client,
chatId
)
} else {
client.send(
"ERROR|" +
Util.encode(
"You are not a member of this chat."
)
)
}
return
}
// ======================================================
// SEND MESSAGE
// ======================================================
if (
command ==
"SEND_MESSAGE"
) {
if (
p.length < 3
) {
return
}
val chatId =
Util.decode(
p(1)
)
val message =
Util.clean(
Util.decode(
p(2)
),
2000
)
val allowed =
chatMembers
.get(chatId)
.exists {
members =>
members.contains(
currentUser
)
}
val directBlocked =
if (chatId.startsWith("DM:")) {
chatMembers.get(chatId).exists { members =>
members.exists { member =>
!member.equalsIgnoreCase(currentUser) &&
isMutuallyBlocked(currentUser, member)
}
}
} else {
false
}
if (
allowed &&
!directBlocked &&
message.nonEmpty
) {
val item =
ChatMessage(
currentUser,
message,
Util.now()
)
val list =
chatHistory.getOrElseUpdate(
chatId,
mutable.ListBuffer[
ChatMessage
]()
)
list +=
item
while (
list.length > 300
) {
list.remove(0)
}
broadcastChat(
chatId,
"MESSAGE|" +
Util.encode(
chatId
) +
"|" +
Util.encode(
item.sender
) +
"|" +
Util.encode(
item.time
) +
"|" +
Util.encode(
item.text
)
)
}
return
}
// ======================================================
// LOGOUT
// ======================================================
if (command == "LOGOUT") {
lock.synchronized {
val name = client.username
if (name != null && name.nonEmpty) {
sessions.get(name.toLowerCase) match {
case Some(current) if current eq client =>
sessions.remove(name.toLowerCase)
broadcastPresence(name)
case _ =>
}
}
}
client.send("LOGOUT_OK")
client.close()
return
}
// ======================================================
// PING
// ======================================================
if (
command ==
"PING"
) {
client.send(
"PONG"
)
return
}
}
private def searchNotifyAll(): Unit = {
sessions.values.foreach { c =>
if (c.username != null && c.username.nonEmpty) {
c.send("PROFILE_CHANGED|" + Util.encode(c.username))
}
}
}
// ========================================================
// PRESENCE
// ========================================================
private def broadcastPresence(
username: String
): Unit = {
val state =
if (
sessions.contains(
username.toLowerCase
)
) {
"1"
} else {
"0"
}
val line =
"PRESENCE|" +
Util.encode(
username
) +
"|" +
state
sessions.values.foreach {
client =>
client.send(
line
)
if (client.username != null && client.username.nonEmpty) {
sendFriendsList(client.username)
}
}
}
// ========================================================
// PROFILE NOTIFICATION
// ========================================================
private def onlineUsersNotify(
username: String
): Unit = {
sessions.values.foreach {
client =>
client.send(
"PROFILE_CHANGED|" +
Util.encode(
username
)
)
}
}
}
// ==========================================================
// CLIENT
// ==========================================================
class ChatClient {
var socket:
Socket = null
var reader:
BufferedReader = null
var writer:
PrintWriter = null
var connected =
false
@volatile private var heartbeatRunning =
false
@volatile private var intentionalClose =
false
var username =
""
var password =
""
var registerUsername =
""
var registerPassword =
""
var registerBio =
""
var registerAvatar =
""
var recoveryUsername =
""
var recoverySecret =
""
var recoveryNewPassword =
""
var ownBio =
""
var ownAvatar =
""
var callback:
String => Unit =
(_: String) => {}
// ========================================================
// CONNECT
// ========================================================
def connect(
host: String,
port: Int
): Boolean = {
intentionalClose = false
if (
connected
) {
return true
}
try {
socket =
new Socket()
socket.setKeepAlive(
true
)
socket.setTcpNoDelay(
true
)
socket.connect(
new InetSocketAddress(
host,
port
),
7000
)
reader =
new BufferedReader(
new InputStreamReader(
socket.getInputStream,
StandardCharsets.UTF_8
)
)
writer =
new PrintWriter(
new OutputStreamWriter(
socket.getOutputStream,
StandardCharsets.UTF_8
),
true
)
connected =
true
val thread =
new Thread(
new Runnable {
override def run(): Unit = {
readLoop()
}
}
)
thread.setDaemon(
true
)
thread.start()
startHeartbeat()
true
} catch {
case _: Throwable =>
connected =
false
false
}
}
private def startHeartbeat(): Unit = {
if (heartbeatRunning) return
heartbeatRunning = true
val t = new Thread(
new Runnable {
override def run(): Unit = {
while (heartbeatRunning) {
try {
Thread.sleep(15000L)
} catch {
case _: Throwable =>
}
if (connected) {
send("PING")
}
}
}
},
"Yadnesh-Heartbeat"
)
t.setDaemon(true)
t.start()
}
// ========================================================
// SEND
// ========================================================
def send(
line: String
): Unit = {
if (
connected &&
writer != null
) {
try {
writer.println(
line
)
writer.flush()
} catch {
case _: Throwable =>
}
}
}
// ========================================================
// CLOSE
// ========================================================
def close(): Unit = {
intentionalClose = true
heartbeatRunning =
false
connected =
false
try {
if (
socket != null
) {
socket.close()
}
} catch {
case _: Throwable =>
}
}
// ========================================================
// READ LOOP
// ========================================================
private def readLoop():
Unit = {
try {
while (
connected
) {
val line =
reader.readLine()
if (
line == null
) {
throw new IOException(
"Disconnected"
)
}
val message =
line
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
processEvent(
message
)
}
}
)
}
} catch {
case _: Throwable =>
connected =
false
heartbeatRunning =
false
if (!intentionalClose) {
try {
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
callback("CLIENT_DISCONNECTED|" + Util.encode("Server connection lost."))
}
}
)
} catch {
case _: Throwable =>
}
}
}
}
// ========================================================
// EVENTS
// ========================================================
private def processEvent(
line: String
): Unit = {
val p =
line.split(
"\\|",
-1
)
if (
p.length == 0
) {
return
}
p(0) match {
// ----------------------------------------------------
// LOGIN CHALLENGE
// ----------------------------------------------------
case "LOGIN_CHALLENGE" =>
if (
p.length >= 3
) {
val salt =
Util.decode(
p(1)
)
val nonce =
Util.decode(
p(2)
)
val verifier =
Util.sha256(
salt +
password
)
val proof =
Util.sha256(
verifier +
nonce
)
send(
"LOGIN_FINISH|" +
Util.encode(
username
) +
"|" +
Util.encode(
proof
)
)
}
// ----------------------------------------------------
// REGISTER CHALLENGE
// ----------------------------------------------------
case "REGISTER_CHALLENGE" =>
if (
p.length >= 2
) {
val salt =
Util.decode(
p(1)
)
val verifier =
Util.sha256(
salt +
registerPassword
)
// Empty field kept for protocol compatibility.
send(
"REGISTER_FINISH|" +
Util.encode(
registerUsername
) +
"|" +
Util.encode(
verifier
) +
"|" +
Util.encode(
registerBio
) +
"|" +
Util.encode(
registerAvatar
) +
"|" +
Util.encode(
"END"
)
)
}
// ----------------------------------------------------
// RECOVERY CHALLENGE
// ----------------------------------------------------
case "RECOVERY_CHALLENGE" =>
if (
p.length >= 2
) {
val nonce =
Util.decode(
p(1)
)
val recoveryHash =
Util.sha256(
recoverySecret
)
val proof =
Util.sha256(
recoveryHash +
nonce
)
val newSalt =
Util.randomHex(
16
)
val newVerifier =
Util.sha256(
newSalt +
recoveryNewPassword
)
send(
"RECOVERY_FINISH|" +
Util.encode(
recoveryUsername
) +
"|" +
Util.encode(
proof
) +
"|" +
Util.encode(
newSalt
) +
"|" +
Util.encode(
newVerifier
)
)
recoverySecret =
""
recoveryNewPassword =
""
}
// ----------------------------------------------------
// LOGIN OK
// ----------------------------------------------------
case "LOGIN_OK" =>
if (
p.length >= 2
) {
username =
Util.decode(
p(1)
)
password =
""
callback(
line
)
}
case "RECOVERY_OK" =>
callback(line)
case "SEARCH_RESULT" =>
callback(line)
case "PROFILE_DATA" =>
callback(line)
case "PROFILE_SAVED" =>
callback(line)
case "PROFILE_CHANGED" =>
callback(line)
case "CHAT" =>
callback(line)
case "HISTORY" =>
callback(line)
case "MESSAGE" =>
callback(line)
case "PRESENCE" =>
callback(line)
case "FRIENDS_LIST" =>
callback(line)
case "FRIEND_REQUESTS" =>
callback(line)
case "FRIEND_REQUEST_SENT" =>
callback(line)
case "FRIEND_REQUEST_RECEIVED" =>
callback(line)
case "FRIEND_ACCEPTED" =>
callback(line)
case "FRIEND_REQUEST_REJECTED" =>
callback(line)
case "FRIEND_REMOVED" =>
callback(line)
case "TYPING" =>
callback(line)
case "BLOCK_STATE" =>
callback(line)
case "BLOCKED_BY" =>
callback(line)
case "UNBLOCKED_BY" =>
callback(line)
case "LOGOUT_OK" =>
callback(line)
case "CLIENT_DISCONNECTED" =>
callback(line)
case "PONG" =>
()
case "GROUP_CREATED" =>
callback(line)
case "ERROR" =>
val text =
if (
p.length >= 2
) {
Util.decode(
p(1)
)
} else {
"Unknown error."
}
JOptionPane.showMessageDialog(
null,
text,
APP_NAME,
JOptionPane.ERROR_MESSAGE
)
case _ =>
}
}
}
// ==========================================================
// LOGIN FRAME
// ==========================================================
class LoginFrame {
private val frame =
new JFrame(
APP_NAME
)
private val client =
new ChatClient
private val usernameField =
new JTextField()
private val passwordField =
new JPasswordField()
private val networkStatus =
new JLabel(
"NETWORK: STARTING...",
SwingConstants.CENTER
)
@volatile private var networkReady =
false
private var discoveredHost =
"127.0.0.1"
private var discoveredPort =
DEFAULT_PORT
// ========================================================
// SHOW
// ========================================================
def show(): Unit = {
Util.setupFont()
frame.setTitle(
APP_NAME +
" - Login"
)
frame.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE
)
frame.setSize(
760,
560
)
frame.setLocationRelativeTo(
null
)
val root =
new JPanel(
new BorderLayout(
15,
15
)
)
root.setBorder(
BorderFactory.createEmptyBorder(
20,
20,
20,
20
)
)
// ------------------------------------------------------
// TITLE
// ------------------------------------------------------
val title =
new JLabel(
"<html>" +
"<center>" +
"<font size='6'><b>YADNESH LIVE CHAT</b></font><br>" +
"<font size='3'>Automatic Wi-Fi PC-to-PC Live Chat</font>" +
"</center>" +
"</html>",
SwingConstants.CENTER
)
root.add(
title,
BorderLayout.NORTH
)
// ------------------------------------------------------
// LOGIN FORM
// ------------------------------------------------------
val form =
new JPanel(
new GridLayout(
2,
2,
10,
10
)
)
form.setBorder(
BorderFactory.createEmptyBorder(
30,
30,
20,
30
)
)
form.add(
new JLabel(
"Username / ID:"
)
)
form.add(
usernameField
)
form.add(
new JLabel(
"Password:"
)
)
form.add(
passwordField
)
val center =
new JPanel(
new BorderLayout(
8,
8
)
)
center.add(
form,
BorderLayout.CENTER
)
networkStatus.setBorder(
BorderFactory.createEmptyBorder(
8,
8,
8,
8
)
)
center.add(
networkStatus,
BorderLayout.SOUTH
)
root.add(
center,
BorderLayout.CENTER
)
// ------------------------------------------------------
// BUTTONS
// ------------------------------------------------------
val loginButton =
new JButton(
"LOGIN"
)
val registerButton =
new JButton(
"CREATE NEW ACCOUNT"
)
val forgotButton =
new JButton(
"FORGOT PASSWORD"
)
val buttons =
new JPanel(
new GridLayout(
1,
3,
8,
8
)
)
buttons.add(
loginButton
)
buttons.add(
registerButton
)
buttons.add(
forgotButton
)
root.add(
buttons,
BorderLayout.SOUTH
)
// ======================================================
// LOGIN
// ======================================================
loginButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
doLogin()
}
}
)
// ======================================================
// REGISTER
// ======================================================
registerButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
doRegister()
}
}
)
// ======================================================
// FORGOT PASSWORD
// ======================================================
forgotButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
forgotPassword()
}
}
)
frame.setContentPane(
root
)
frame.setVisible(
true
)
startAutomaticNetwork()
}
// ========================================================
// AUTOMATIC NETWORK
// ========================================================
private def startAutomaticNetwork(): Unit = {
networkStatus.setText(
"NETWORK: SEARCHING FOR HOST PC..."
)
val thread =
new Thread(
new Runnable {
override def run(): Unit = {
var found:
Option[(String, Int)] =
None
val deadline =
System.currentTimeMillis() +
2500L
while (
found.isEmpty &&
System.currentTimeMillis() <
deadline
) {
found =
LanDiscovery.findServer(
700
)
}
found match {
case Some(
(host, port)
) =>
discoveredHost =
host
discoveredPort =
port
val ok =
client.connect(
host,
port
)
networkReady =
ok
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
if (ok) {
networkStatus.setText(
"NETWORK: CONNECTED AUTOMATICALLY ? " +
host +
":" +
port
)
} else {
networkStatus.setText(
"NETWORK: HOST FOUND BUT CONNECTION FAILED"
)
}
}
}
)
case None =>
// No host was found, so this PC becomes
// the first host automatically.
val started =
Server.start(
DEFAULT_PORT
)
if (
started
) {
discoveredHost =
"127.0.0.1"
discoveredPort =
DEFAULT_PORT
val ok =
client.connect(
"127.0.0.1",
DEFAULT_PORT
)
networkReady =
ok
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
if (ok) {
networkStatus.setText(
"NETWORK: THIS PC IS HOST ? " +
Util.localIPs().mkString(
", "
) +
":" +
DEFAULT_PORT
)
} else {
networkStatus.setText(
"NETWORK: HOST STARTED BUT LOCAL CONNECTION FAILED"
)
}
}
}
)
} else {
// A server may have appeared just after the
// discovery timeout. Try discovery once more.
val retry =
LanDiscovery.findServer(
1500
)
retry match {
case Some(
(host, port)
) =>
discoveredHost =
host
discoveredPort =
port
val ok =
client.connect(
host,
port
)
networkReady =
ok
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
networkStatus.setText(
if (ok) {
"NETWORK: CONNECTED ? " +
host +
":" +
port
} else {
"NETWORK: CONNECTION FAILED"
}
)
}
}
)
case None =>
networkReady =
false
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
networkStatus.setText(
"NETWORK: NOT AVAILABLE ? START HOST ON ONE PC"
)
}
}
)
}
}
}
}
},
"Yadnesh-Auto-Network"
)
thread.setDaemon(
true
)
thread.start()
}
// ========================================================
// CONNECT
// ========================================================
private def connect(): Boolean = {
if (
client.connected
) {
return true
}
if (
!networkReady
) {
JOptionPane.showMessageDialog(
frame,
"Network is still starting. Please wait a moment.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return false
}
val ok =
client.connect(
discoveredHost,
discoveredPort
)
if (
!ok
) {
networkReady =
false
networkStatus.setText(
"NETWORK: DISCONNECTED"
)
}
ok
}
// ========================================================
// LOGIN
// ========================================================
private def doLogin(): Unit = {
val name =
usernameField
.getText
.trim
val pass =
new String(
passwordField
.getPassword
)
if (
!Util.validUsername(
name
)
) {
JOptionPane.showMessageDialog(
frame,
"Username must be 3-20 characters.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (
pass.isEmpty
) {
JOptionPane.showMessageDialog(
frame,
"Enter password.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (
connect()
) {
client.username =
name
client.password =
pass
client.callback =
loginEvent
client.send(
"LOGIN_START|" +
Util.encode(
name
)
)
}
}
// ========================================================
// LOGIN EVENT
// ========================================================
private def loginEvent(
line: String
): Unit = {
if (
line.startsWith(
"LOGIN_OK|"
)
) {
openMain()
}
}
// ========================================================
// REGISTER
// ========================================================
private def doRegister(): Unit = {
val dialog =
new JDialog(
frame,
"CREATE NEW ACCOUNT",
true
)
dialog.setSize(
680,
560
)
dialog.setLocationRelativeTo(
frame
)
val username =
new JTextField()
val password =
new JPasswordField()
val confirm =
new JPasswordField()
val bio =
new JTextField(
"Hello! I use Yadnesh Live Chat."
)
val choose =
new JButton(
"CHOOSE IMAGE"
)
val paste =
new JButton(
"PASTE IMAGE"
)
val imageLabel =
new JLabel(
"No image selected"
)
var avatar =
""
val form =
new JPanel(
new GridLayout(
6,
2,
8,
8
)
)
form.setBorder(
BorderFactory.createEmptyBorder(
15,
15,
15,
15
)
)
form.add(
new JLabel(
"Username / ID:"
)
)
form.add(
username
)
form.add(
new JLabel(
"Password:"
)
)
form.add(
password
)
form.add(
new JLabel(
"Confirm Password:"
)
)
form.add(
confirm
)
form.add(
new JLabel(
"Public Bio:"
)
)
form.add(
bio
)
form.add(
new JLabel(
"Profile Image:"
)
)
val imagePanel =
new JPanel(
new BorderLayout(
5,
5
)
)
val imageButtons =
new JPanel(
new GridLayout(
1,
2,
5,
5
)
)
imageButtons.add(
choose
)
imageButtons.add(
paste
)
imagePanel.add(
imageButtons,
BorderLayout.WEST
)
imagePanel.add(
imageLabel,
BorderLayout.CENTER
)
form.add(
imagePanel
)
val create =
new JButton(
"CREATE ACCOUNT"
)
form.add(
new JLabel(
""
)
)
form.add(
create
)
choose.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val chooser =
new JFileChooser()
if (
chooser.showOpenDialog(
dialog
) ==
JFileChooser.APPROVE_OPTION
) {
val encoded =
Util.fileToBase64(
chooser
.getSelectedFile
.getAbsolutePath
)
if (
encoded.nonEmpty
) {
avatar =
encoded
imageLabel.setText(
"Image selected"
)
} else {
JOptionPane.showMessageDialog(
dialog,
"Image invalid or larger than about 350 KB.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
}
}
}
}
)
paste.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val encoded =
Util.clipboardImageBase64()
if (
encoded.nonEmpty
) {
avatar =
encoded
imageLabel.setText(
"Pasted image saved"
)
} else {
JOptionPane.showMessageDialog(
dialog,
"Clipboard does not contain an image.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
}
}
}
)
create.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val name =
username
.getText
.trim
val pass =
new String(
password
.getPassword
)
val confirmPass =
new String(
confirm
.getPassword
)
if (
!Util.validUsername(
name
)
) {
JOptionPane.showMessageDialog(
dialog,
"Username must be 3-20 characters.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (
pass.length < 4
) {
JOptionPane.showMessageDialog(
dialog,
"Password needs at least 4 characters.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (
pass != confirmPass
) {
JOptionPane.showMessageDialog(
dialog,
"Passwords do not match.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (
!connect()
) {
return
}
client.registerUsername =
name
client.registerPassword =
pass
client.registerBio =
bio.getText.trim
client.registerAvatar =
avatar
client.callback =
registerEvent
client.send(
"REGISTER_START|" +
Util.encode(
name
)
)
dialog.dispose()
}
}
)
dialog.setContentPane(
form
)
dialog.setVisible(
true
)
}
// ========================================================
// REGISTER EVENT
// ========================================================
private def registerEvent(
line: String
): Unit = {
if (
line.startsWith(
"LOGIN_OK|"
)
) {
openMain()
}
}
// ========================================================
// FORGOT PASSWORD
// ========================================================
private def forgotPassword(): Unit = {
val dialog =
new JDialog(
frame,
"FORGOT PASSWORD",
true
)
dialog.setSize(
570,
420
)
dialog.setLocationRelativeTo(
frame
)
val username =
new JTextField()
val recovery =
new JPasswordField()
val newPassword =
new JPasswordField()
val confirm =
new JPasswordField()
val reset =
new JButton(
"RESET PASSWORD"
)
val note =
new JLabel(
"<html>" +
"Enter your Username / ID, private recovery secret,<br>" +
"and choose a new password." +
"</html>"
)
val form =
new JPanel(
new GridLayout(
5,
2,
8,
8
)
)
form.setBorder(
BorderFactory.createEmptyBorder(
15,
15,
15,
15
)
)
form.add(
new JLabel(
"Info:"
)
)
form.add(
note
)
form.add(
new JLabel(
"Username / ID:"
)
)
form.add(
username
)
form.add(
new JLabel(
"Recovery Secret:"
)
)
form.add(
recovery
)
form.add(
new JLabel(
"New Password:"
)
)
form.add(
newPassword
)
form.add(
new JLabel(
"Confirm:"
)
)
form.add(
confirm
)
val panel =
new JPanel(
new BorderLayout(
8,
8
)
)
panel.add(
form,
BorderLayout.CENTER
)
panel.add(
reset,
BorderLayout.SOUTH
)
reset.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val name =
username
.getText
.trim
val secret =
new String(
recovery
.getPassword
)
val newPass =
new String(
newPassword
.getPassword
)
val confirmPass =
new String(
confirm
.getPassword
)
if (
!Util.validUsername(
name
)
) {
JOptionPane.showMessageDialog(
dialog,
"Enter valid username.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (
secret.isEmpty
) {
JOptionPane.showMessageDialog(
dialog,
"Enter recovery secret.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (
newPass.length < 4
) {
JOptionPane.showMessageDialog(
dialog,
"New password needs at least 4 characters.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (
newPass != confirmPass
) {
JOptionPane.showMessageDialog(
dialog,
"New passwords do not match.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
if (
!connect()
) {
return
}
client.recoveryUsername =
name
client.recoverySecret =
secret
client.recoveryNewPassword =
newPass
client.callback =
recoveryEvent
client.send(
"RECOVERY_START|" +
Util.encode(
name
)
)
dialog.dispose()
}
}
)
dialog.setContentPane(
panel
)
dialog.setVisible(
true
)
}
// ========================================================
// RECOVERY EVENT
// ========================================================
private def recoveryEvent(
line: String
): Unit = {
if (
line ==
"RECOVERY_OK"
) {
JOptionPane.showMessageDialog(
frame,
"Password reset successfully.\nYou can now login.",
APP_NAME,
JOptionPane.INFORMATION_MESSAGE
)
client.callback =
loginEvent
}
}
// ========================================================
// OPEN MAIN
// ========================================================
private def openMain(): Unit = {
frame.dispose()
val main =
new MainFrame(
client
)
client.callback =
main.handleServerEvent
main.show()
client.send(
"PROFILE|" +
Util.encode(
client.username
)
)
client.send(
"SEARCH|" +
Util.encode(
""
)
)
}
}
// ==========================================================
// MAIN CHAT WINDOW
// ==========================================================
class MainFrame(
client: ChatClient
) {
private val frame =
new JFrame(
APP_NAME +
" - " +
client.username
)
private val peopleModel =
new DefaultListModel[
PersonItem
]()
private val friendsModel =
new DefaultListModel[
PersonItem
]()
private val requestModel =
new DefaultListModel[
PersonItem
]()
private val chatModel =
new DefaultListModel[
ChatItem
]()
private val peopleList =
new JList[
PersonItem
](
peopleModel
)
private val friendsList =
new JList[
PersonItem
](
friendsModel
)
private val chatList =
new JList[
ChatItem
](
chatModel
)
private val searchField =
new JTextField()
private val chatArea =
new JTextArea()
private val messageField =
new JTextField()
private val sendButton =
new JButton(
"SEND"
)
private val exportButton =
new JButton(
"EXPORT CHAT"
)
private val muteButton =
new JButton(
"MUTE CHAT"
)
private val logoutButton =
new JButton(
"LOGOUT"
)
private val blockButton =
new JButton(
"BLOCK USER"
)
private val chatTitle =
new JLabel(
"Select a chat"
)
private val profileName =
new JLabel(
"No person selected"
)
private val profileStatus =
new JLabel(
""
)
private val typingLabel =
new JLabel(
""
)
private val onlineSummary =
new JLabel(
" LIVE"
)
private val profileBio =
new JTextArea()
private val profileImage =
new JLabel(
"No Photo",
SwingConstants.CENTER
)
private val openChatButton =
new JButton(
"OPEN FRIEND CHAT"
)
private val friendButton =
new JButton(
"SEND FRIEND REQUEST"
)
private val acceptButton =
new JButton(
"ACCEPT REQUEST"
)
private val rejectButton =
new JButton(
"REJECT REQUEST"
)
private val removeFriendButton =
new JButton(
"REMOVE FRIEND"
)
private val friendsButton =
new JButton(
"REFRESH FRIENDS"
)
private val requestsButton =
new JButton(
"FRIEND REQUESTS"
)
private val groupButton =
new JButton(
"CREATE GROUP"
)
private val editButton =
new JButton(
"EDIT MY PROFILE"
)
private var selectedPerson =
""
private var currentChat =
""
private val chatMap =
mutable.Map[
String,
ChatItem
]()
private val unreadMap =
mutable.Map[String, Int]()
private val mutedChats =
mutable.Set[String]()
private val typingTimer =
new javax.swing.Timer(900, new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (currentChat != null && currentChat.nonEmpty) {
client.send("TYPING|" + Util.encode(currentChat) + "|0")
}
}
})
// ========================================================
// SHOW
// ========================================================
def show(): Unit = {
Util.setupFont()
updateFriendButtons("NONE")
frame.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE
)
frame.setSize(
1250,
760
)
frame.setMinimumSize(
new Dimension(
1000,
650
)
)
frame.setLocationRelativeTo(
null
)
val root =
new JPanel(
new BorderLayout(
7,
7
)
)
// ======================================================
// TOP BAR
// ======================================================
val top =
new JPanel(
new BorderLayout()
)
top.setBackground(
new Color(
33,
38,
46
)
)
val title =
new JLabel(
" YADNESH LIVE CHAT"
)
title.setForeground(
Color.WHITE
)
title.setFont(
new Font(
"Segoe UI",
Font.BOLD,
22
)
)
val userText =
new JLabel(
"Logged in: " +
client.username +
" "
)
userText.setForeground(
Color.WHITE
)
top.add(
title,
BorderLayout.WEST
)
val topRight =
new JPanel(
new BorderLayout(8, 0)
)
topRight.setOpaque(false)
topRight.add(onlineSummary, BorderLayout.CENTER)
topRight.add(userText, BorderLayout.WEST)
topRight.add(logoutButton, BorderLayout.EAST)
onlineSummary.setForeground(Color.WHITE)
logoutButton.setFocusable(false)
top.add(
topRight,
BorderLayout.EAST
)
root.add(
top,
BorderLayout.NORTH
)
// ======================================================
// LEFT
// ======================================================
val left =
new JPanel(
new BorderLayout(
5,
5
)
)
left.setPreferredSize(
new Dimension(
300,
100
)
)
val searchPanel =
new JPanel(
new BorderLayout(
5,
5
)
)
val searchButton =
new JButton(
"SEARCH"
)
searchPanel.add(
searchField,
BorderLayout.CENTER
)
searchPanel.add(
searchButton,
BorderLayout.EAST
)
left.add(
searchPanel,
BorderLayout.NORTH
)
// ------------------------------------------------------
// PEOPLE
// ------------------------------------------------------
peopleList.setSelectionMode(
ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
)
val peoplePanel =
new JPanel(
new BorderLayout(
3,
3
)
)
peoplePanel.add(
new JLabel(
" PEOPLE"
),
BorderLayout.NORTH
)
peoplePanel.add(
new JScrollPane(
peopleList
),
BorderLayout.CENTER
)
// ------------------------------------------------------
// CHATS
// ------------------------------------------------------
val friendsPanel =
new JPanel(
new BorderLayout(
3,
3
)
)
friendsPanel.add(
new JLabel(
" FRIENDS"
),
BorderLayout.NORTH
)
friendsPanel.add(
new JScrollPane(
friendsList
),
BorderLayout.CENTER
)
val chatPanel =
new JPanel(
new BorderLayout(
3,
3
)
)
chatPanel.add(
new JLabel(
" MY CHATS"
),
BorderLayout.NORTH
)
chatPanel.add(
new JScrollPane(
chatList
),
BorderLayout.CENTER
)
val leftCenter =
new JPanel(
new GridLayout(
3,
1,
5,
5
)
)
leftCenter.add(
peoplePanel
)
leftCenter.add(
friendsPanel
)
leftCenter.add(
chatPanel
)
left.add(
leftCenter,
BorderLayout.CENTER
)
val leftActions =
new JPanel(
new GridLayout(
1,
3,
4,
4
)
)
leftActions.add(
groupButton
)
leftActions.add(
friendsButton
)
leftActions.add(
requestsButton
)
left.add(
leftActions,
BorderLayout.SOUTH
)
root.add(
left,
BorderLayout.WEST
)
// ======================================================
// CENTER
// ======================================================
val center =
new JPanel(
new BorderLayout(
5,
5
)
)
chatTitle.setFont(
new Font(
"Segoe UI",
Font.BOLD,
18
)
)
val chatHeader =
new JPanel(
new BorderLayout(5, 2)
)
chatHeader.add(
chatTitle,
BorderLayout.WEST
)
chatHeader.add(
typingLabel,
BorderLayout.CENTER
)
center.add(
chatHeader,
BorderLayout.NORTH
)
chatArea.setEditable(
false
)
chatArea.setLineWrap(
true
)
chatArea.setWrapStyleWord(
true
)
chatArea.setFont(
new Font(
"Segoe UI",
Font.PLAIN,
14
)
)
val messageScroll =
new JScrollPane(
chatArea
)
center.add(
messageScroll,
BorderLayout.CENTER
)
val sendPanel =
new JPanel(
new BorderLayout(
5,
5
)
)
sendPanel.add(
messageField,
BorderLayout.CENTER
)
val sendTools =
new JPanel(
new GridLayout(1, 3, 4, 4)
)
sendTools.add(sendButton)
sendTools.add(exportButton)
sendTools.add(muteButton)
sendPanel.add(
sendTools,
BorderLayout.EAST
)
center.add(
sendPanel,
BorderLayout.SOUTH
)
root.add(
center,
BorderLayout.CENTER
)
// ======================================================
// RIGHT PROFILE
// ======================================================
val right =
new JPanel(
new BorderLayout(
5,
5
)
)
right.setPreferredSize(
new Dimension(
285,
100
)
)
right.add(
new JLabel(
" PROFILE"
),
BorderLayout.NORTH
)
profileImage.setPreferredSize(
new Dimension(
235,
180
)
)
profileImage.setBorder(
BorderFactory.createLineBorder(
new Color(
180,
180,
180
)
)
)
val profileText =
new JPanel(
new BorderLayout(
5,
5
)
)
val names =
new JPanel(
new GridLayout(
2,
1,
3,
3
)
)
names.add(
profileName
)
names.add(
profileStatus
)
profileText.add(
names,
BorderLayout.NORTH
)
profileBio.setEditable(
false
)
profileBio.setLineWrap(
true
)
profileBio.setWrapStyleWord(
true
)
profileText.add(
new JScrollPane(
profileBio
),
BorderLayout.CENTER
)
val profileCenter =
new JPanel(
new BorderLayout(
5,
5
)
)
profileCenter.add(
profileImage,
BorderLayout.NORTH
)
profileCenter.add(
profileText,
BorderLayout.CENTER
)
right.add(
profileCenter,
BorderLayout.CENTER
)
val profileButtons =
new JPanel(
new GridLayout(
7,
1,
5,
5
)
)
profileButtons.add(openChatButton)
profileButtons.add(friendButton)
profileButtons.add(acceptButton)
profileButtons.add(rejectButton)
profileButtons.add(removeFriendButton)
profileButtons.add(blockButton)
profileButtons.add(editButton)
right.add(
profileButtons,
BorderLayout.SOUTH
)
root.add(
right,
BorderLayout.EAST
)
// ======================================================
// SEARCH
// ======================================================
searchButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
searchPeople()
}
}
)
searchField.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
searchPeople()
}
}
)
// ======================================================
// PERSON SELECT
// ======================================================
peopleList.addListSelectionListener(
_ => {
val person =
peopleList
.getSelectedValue
if (
person != null
) {
selectedPerson =
person.username
profileName.setText(
person.username
)
profileStatus.setText(
if (person.online) {
"? ONLINE"
} else {
"? OFFLINE"
}
)
profileBio.setText(
if (
person.bio.isEmpty
) {
"No public bio."
} else {
person.bio
}
)
client.send(
"PROFILE|" +
Util.encode(
person.username
)
)
}
}
)
// ======================================================
// DOUBLE CLICK
// ======================================================
peopleList.addMouseListener(
new MouseAdapter {
override def mouseClicked(
e: MouseEvent
): Unit = {
if (
e.getClickCount == 2
) {
val person =
peopleList
.getSelectedValue
if (
person != null
) {
openDirectChat(
person.username
)
}
}
}
}
)
// ======================================================
// FRIEND ACTIONS
// ======================================================
friendButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (selectedPerson.nonEmpty) {
client.send(
"FRIEND_REQUEST|" + Util.encode(selectedPerson)
)
}
}
}
)
acceptButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (selectedPerson.nonEmpty) {
client.send(
"FRIEND_ACCEPT|" + Util.encode(selectedPerson)
)
}
}
}
)
rejectButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (selectedPerson.nonEmpty) {
client.send(
"FRIEND_REJECT|" + Util.encode(selectedPerson)
)
}
}
}
)
removeFriendButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (selectedPerson.nonEmpty) {
client.send(
"FRIEND_REMOVE|" + Util.encode(selectedPerson)
)
}
}
}
)
friendsButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
refreshFriends()
}
}
)
requestsButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
client.send("FRIEND_REQUESTS")
}
}
)
friendsList.addListSelectionListener(
_ => {
val person = friendsList.getSelectedValue
if (person != null) {
selectedPerson = person.username
profileName.setText(person.username)
profileStatus.setText(if (person.online) "? ONLINE" else "? OFFLINE")
profileBio.setText(if (person.bio.isEmpty) "No public bio." else person.bio)
client.send("PROFILE|" + Util.encode(person.username))
}
}
)
friendsList.addMouseListener(
new MouseAdapter {
override def mouseClicked(e: MouseEvent): Unit = {
if (e.getClickCount == 2) {
val person = friendsList.getSelectedValue
if (person != null) openDirectChat(person.username)
}
}
}
)
// ======================================================
// OPEN DIRECT CHAT
// ======================================================
openChatButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
if (
selectedPerson != null &&
selectedPerson.nonEmpty
) {
openDirectChat(
selectedPerson
)
}
}
}
)
// ======================================================
// CHAT LIST
// ======================================================
chatList.addListSelectionListener(
_ => {
val chat =
chatList
.getSelectedValue
if (
chat != null
) {
currentChat =
chat.id
unreadMap.remove(chat.id)
val cleared = chat.copy(unread = 0)
chatMap.put(chat.id, cleared)
replaceChatItem(cleared)
typingLabel.setText("")
typingTimer.stop()
client.send("TYPING|" + Util.encode(chat.id) + "|0")
muteButton.setText(if (mutedChats.contains(chat.id)) "UNMUTE CHAT" else "MUTE CHAT")
chatTitle.setText(
chat.title
)
chatArea.setText(
"Loading messages..."
)
client.send(
"OPEN_CHAT|" +
Util.encode(
chat.id
)
)
}
}
)
// ======================================================
// SEND
// ======================================================
sendButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
sendMessage()
}
}
)
messageField.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
sendMessage()
}
}
)
// ======================================================
// EDIT
// ======================================================
editButton.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
editProfile()
}
}
)
blockButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (selectedPerson.nonEmpty) {
if (blockButton.getText == "UNBLOCK USER") {
client.send("UNBLOCK_USER|" + Util.encode(selectedPerson))
} else {
val ok = JOptionPane.showConfirmDialog(
frame,
"Block " + selectedPerson + "? Friend requests and direct chat will be stopped.",
APP_NAME,
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE
)
if (ok == JOptionPane.YES_OPTION) {
client.send("BLOCK_USER|" + Util.encode(selectedPerson))
}
}
}
}
}
)
logoutButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
val ok = JOptionPane.showConfirmDialog(
frame,
"Logout from " + client.username + "?",
APP_NAME,
JOptionPane.YES_NO_OPTION
)
if (ok == JOptionPane.YES_OPTION) {
client.send("LOGOUT")
client.close()
frame.dispose()
new LoginFrame().show()
}
}
}
)
exportButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
exportCurrentChat()
}
}
)
muteButton.addActionListener(
new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
if (currentChat != null && currentChat.nonEmpty) {
if (mutedChats.contains(currentChat)) {
mutedChats.remove(currentChat)
muteButton.setText("MUTE CHAT")
} else {
mutedChats.add(currentChat)
muteButton.setText("UNMUTE CHAT")
}
}
}
}
)
messageField.getDocument.addDocumentListener(
new DocumentListener {
private def changed(): Unit = {
if (currentChat != null && currentChat.nonEmpty && messageField.getText.nonEmpty) {
typingTimer.restart()
client.send(
"TYPING|" + Util.encode(currentChat) + "|1"
)
}
}
override def insertUpdate(e: DocumentEvent): Unit = changed()
override def removeUpdate(e: DocumentEvent): Unit = changed()
override def changedUpdate(e: DocumentEvent): Unit = changed()
}
)
frame.setContentPane(
root
)
frame.setVisible(
true
)
refreshFriends()
client.send("FRIEND_REQUESTS")
searchPeople()
}
private def replaceChatItem(item: ChatItem): Unit = {
chatMap.put(item.id, item)
var i = 0
while (i < chatModel.size()) {
val old = chatModel.getElementAt(i)
if (old.id == item.id) {
chatModel.setElementAt(item, i)
return
}
i += 1
}
chatModel.addElement(item)
}
// ========================================================
// SEARCH
// ========================================================
private def searchPeople(): Unit = {
client.send(
"SEARCH|" +
Util.encode(
searchField
.getText
.trim
)
)
}
// ========================================================
// REFRESH FRIENDS
// ========================================================
private def refreshFriends(): Unit = {
client.send(
"FRIENDS_LIST"
)
}
// ========================================================
// DIRECT CHAT
// ========================================================
private def openDirectChat(
username: String
): Unit = {
client.send(
"OPEN_DM|" +
Util.encode(
username
)
)
}
// ========================================================
// SEND MESSAGE
// ========================================================
private def sendMessage(): Unit = {
if (
currentChat == null ||
currentChat.isEmpty
) {
JOptionPane.showMessageDialog(
frame,
"First select a chat.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val message =
messageField
.getText
.trim
if (
message.isEmpty
) {
return
}
typingTimer.stop()
client.send("TYPING|" + Util.encode(currentChat) + "|0")
client.send(
"SEND_MESSAGE|" +
Util.encode(
currentChat
) +
"|" +
Util.encode(
message
)
)
messageField.setText(
""
)
}
private def exportCurrentChat(): Unit = {
if (currentChat == null || currentChat.isEmpty) {
JOptionPane.showMessageDialog(
frame,
"First open a chat.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val chooser = new JFileChooser()
chooser.setDialogTitle("EXPORT CHAT")
chooser.setSelectedFile(new java.io.File("YadneshChat_" + System.currentTimeMillis() + ".txt"))
if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
try {
Files.write(
chooser.getSelectedFile.toPath,
chatArea.getText.getBytes(StandardCharsets.UTF_8)
)
JOptionPane.showMessageDialog(
frame,
"Chat exported successfully.",
APP_NAME,
JOptionPane.INFORMATION_MESSAGE
)
} catch {
case _: Throwable =>
JOptionPane.showMessageDialog(
frame,
"Could not export chat.",
APP_NAME,
JOptionPane.ERROR_MESSAGE
)
}
}
}
// ========================================================
// CREATE GROUP
// ========================================================
private def createGroup(): Unit = {
val selected =
peopleList
.getSelectedValuesList()
if (
selected == null ||
selected.size() < 1
) {
JOptionPane.showMessageDialog(
frame,
"Select at least one person.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
return
}
val name =
JOptionPane.showInputDialog(
frame,
"Group name:",
"CREATE GROUP",
JOptionPane.QUESTION_MESSAGE
)
if (
name == null ||
name.trim.isEmpty
) {
return
}
val request =
new StringBuilder()
request.append(
"CREATE_GROUP|"
)
request.append(
Util.encode(
name.trim
)
)
request.append(
"|" +
selected.size()
)
var i =
0
while (
i < selected.size()
) {
val person =
selected.get(
i
)
request.append(
"|" +
Util.encode(
person.username
)
)
i +=
1
}
client.send(
request.toString
)
}
// ========================================================
// EDIT PROFILE
// ========================================================
private def editProfile(): Unit = {
val dialog =
new JDialog(
frame,
"EDIT MY PROFILE",
true
)
dialog.setSize(
700,
540
)
dialog.setLocationRelativeTo(
frame
)
val bio =
new JTextField(
client.ownBio
)
val choose =
new JButton(
"CHOOSE IMAGE"
)
val paste =
new JButton(
"PASTE IMAGE"
)
val imageLabel =
new JLabel(
if (
client.ownAvatar.nonEmpty
) {
"Current image saved ?"
} else {
"No image"
}
)
var avatar =
client.ownAvatar
val save =
new JButton(
"SAVE PROFILE"
)
val form =
new JPanel(
new GridLayout(
4,
2,
8,
8
)
)
form.setBorder(
BorderFactory.createEmptyBorder(
15,
15,
15,
15
)
)
form.add(
new JLabel(
"Username:"
)
)
form.add(
new JLabel(
client.username
)
)
form.add(
new JLabel(
"Public Bio:"
)
)
form.add(
bio
)
form.add(
new JLabel(
"Profile Image:"
)
)
val imagePanel =
new JPanel(
new BorderLayout(
5,
5
)
)
val imageButtons =
new JPanel(
new GridLayout(
1,
2,
5,
5
)
)
imageButtons.add(
choose
)
imageButtons.add(
paste
)
imagePanel.add(
imageButtons,
BorderLayout.WEST
)
imagePanel.add(
imageLabel,
BorderLayout.CENTER
)
form.add(
imagePanel
)
form.add(
new JLabel(
"Save:"
)
)
form.add(
save
)
choose.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val chooser =
new JFileChooser()
if (
chooser.showOpenDialog(
dialog
) ==
JFileChooser.APPROVE_OPTION
) {
val encoded =
Util.fileToBase64(
chooser
.getSelectedFile
.getAbsolutePath
)
if (
encoded.nonEmpty
) {
avatar =
encoded
imageLabel.setText(
"New image selected ?"
)
} else {
JOptionPane.showMessageDialog(
dialog,
"Image invalid or too large.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
}
}
}
}
)
paste.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
val encoded =
Util.clipboardImageBase64()
if (
encoded.nonEmpty
) {
avatar =
encoded
imageLabel.setText(
"Pasted image saved ?"
)
} else {
JOptionPane.showMessageDialog(
dialog,
"Clipboard does not contain an image.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
}
}
}
)
save.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
client.ownBio =
bio.getText.trim
client.ownAvatar =
avatar
client.send(
"UPDATE_PROFILE|" +
Util.encode(
client.ownBio
) +
"|" +
Util.encode(
client.ownAvatar
)
)
dialog.dispose()
}
}
)
dialog.setContentPane(
form
)
dialog.setVisible(
true
)
}
// ========================================================
// SERVER EVENTS
// ========================================================
def handleServerEvent(
line: String
): Unit = {
val p =
line.split(
"\\|",
-1
)
if (
p.length == 0
) {
return
}
p(0) match {
// ----------------------------------------------------
// SEARCH RESULTS
// ----------------------------------------------------
case "SEARCH_RESULT" =>
peopleModel.clear()
if (
p.length >= 2
) {
val count =
try {
p(1).toInt
} catch {
case _: Throwable =>
0
}
var index =
2
var i =
0
while (
i < count &&
index + 2 <
p.length
) {
val name =
Util.decode(
p(index)
)
val bio =
Util.decode(
p(index + 1)
)
val online =
p(index + 2) ==
"1"
val relation =
if (index + 3 < p.length) p(index + 3) else "NONE"
peopleModel.addElement(
PersonItem(
name,
bio,
online,
relation
)
)
index +=
(if (index + 3 < p.length) 4 else 3)
i +=
1
}
}
// ----------------------------------------------------
// PROFILE
// ----------------------------------------------------
case "PROFILE_DATA" =>
if (
p.length >= 5
) {
val name =
Util.decode(
p(1)
)
val bio =
Util.decode(
p(2)
)
val avatar =
Util.decode(
p(3)
)
val online =
p(4) ==
"1"
val relation =
if (p.length >= 6) p(5) else "NONE"
updateFriendButtons(relation)
if (
name.equalsIgnoreCase(
client.username
)
) {
client.ownBio =
bio
client.ownAvatar =
avatar
}
if (
name.equalsIgnoreCase(
selectedPerson
)
) {
profileName.setText(
name
)
profileStatus.setText(
if (online) {
"? ONLINE"
} else {
"? OFFLINE"
}
)
profileBio.setText(
if (bio.isEmpty) {
"No public bio."
} else {
bio
}
)
displayAvatar(
avatar
)
}
}
// ----------------------------------------------------
// PROFILE SAVED
// ----------------------------------------------------
case "PROFILE_SAVED" =>
JOptionPane.showMessageDialog(
frame,
"Profile saved permanently on the host server.",
APP_NAME,
JOptionPane.INFORMATION_MESSAGE
)
client.send(
"PROFILE|" +
Util.encode(
client.username
)
)
searchPeople()
// ----------------------------------------------------
// PROFILE CHANGED
// ----------------------------------------------------
case "PROFILE_CHANGED" =>
if (
p.length >= 2
) {
val name =
Util.decode(
p(1)
)
if (
name.equalsIgnoreCase(
selectedPerson
)
) {
client.send(
"PROFILE|" +
Util.encode(
selectedPerson
)
)
}
searchPeople()
refreshFriends()
}
// ----------------------------------------------------
// CHAT
// ----------------------------------------------------
case "CHAT" =>
if (
p.length >= 4
) {
val id =
Util.decode(
p(1)
)
val title =
Util.decode(
p(2)
)
val group =
p(3) ==
"1"
if (
!chatMap.contains(
id
)
) {
val entry =
ChatItem(
id,
title,
group,
unreadMap.getOrElse(id, 0)
)
chatMap.put(
id,
entry
)
chatModel.addElement(
entry
)
}
}
// ----------------------------------------------------
// HISTORY
// ----------------------------------------------------
case "HISTORY" =>
if (
p.length >= 3
) {
val id =
Util.decode(
p(1)
)
val count =
try {
p(2).toInt
} catch {
case _: Throwable =>
0
}
if (
id ==
currentChat
) {
val out =
new StringBuilder()
var index =
3
var i =
0
while (
i < count &&
index + 2 <
p.length
) {
val sender =
Util.decode(
p(index)
)
val time =
Util.decode(
p(index + 1)
)
val message =
Util.decode(
p(index + 2)
)
out.append(
"[" +
time +
"] " +
sender +
": " +
message +
"\n"
)
index +=
3
i +=
1
}
chatArea.setText(
out.toString
)
chatArea.setCaretPosition(
chatArea.getDocument
.getLength
)
}
}
// ----------------------------------------------------
// LIVE MESSAGE
// ----------------------------------------------------
case "MESSAGE" =>
if (
p.length >= 5
) {
val id =
Util.decode(
p(1)
)
val sender =
Util.decode(
p(2)
)
val time =
Util.decode(
p(3)
)
val message =
Util.decode(
p(4)
)
if (
id ==
currentChat
) {
typingLabel.setText("")
chatArea.append(
"[" +
time +
"] " +
sender +
": " +
message +
"\n"
)
chatArea.setCaretPosition(
chatArea.getDocument
.getLength
)
} else {
val next = unreadMap.getOrElse(id, 0) + 1
unreadMap.put(id, next)
chatMap.get(id).foreach { old =>
replaceChatItem(old.copy(unread = next))
}
if (!mutedChats.contains(id)) {
Toolkit.getDefaultToolkit.beep()
}
}
}
// ----------------------------------------------------
// TYPING
// ----------------------------------------------------
case "TYPING" =>
if (p.length >= 4) {
val id = Util.decode(p(1))
val name = Util.decode(p(2))
val state = p(3) == "1"
if (id == currentChat && !name.equalsIgnoreCase(client.username)) {
typingLabel.setText(if (state) name + " is typing..." else "")
}
}
// ----------------------------------------------------
// PRESENCE
// ----------------------------------------------------
case "PRESENCE" =>
if (
p.length >= 3
) {
val name =
Util.decode(
p(1)
)
val online =
p(2) ==
"1"
var i =
0
while (
i < peopleModel.size()
) {
val old =
peopleModel
.getElementAt(
i
)
if (
old.username
.equalsIgnoreCase(
name
)
) {
peopleModel.setElementAt(
old.copy(
online =
online
),
i
)
}
i +=
1
}
var j = 0
while (j < friendsModel.size()) {
val oldFriend = friendsModel.getElementAt(j)
if (oldFriend.username.equalsIgnoreCase(name)) {
friendsModel.setElementAt(oldFriend.copy(online = online), j)
}
j += 1
}
if (
selectedPerson
.equalsIgnoreCase(
name
)
) {
profileStatus.setText(
if (online) {
"? ONLINE"
} else {
"? OFFLINE"
}
)
}
}
// ----------------------------------------------------
// FRIENDS LIST
// ----------------------------------------------------
case "FRIENDS_LIST" =>
friendsModel.clear()
if (p.length >= 2) {
val count = try p(1).toInt catch { case _: Throwable => 0 }
var onlineCount = 0
var index = 2
var i = 0
while (i < count && index + 2 < p.length) {
val name = Util.decode(p(index))
val online = p(index + 1) == "1"
if (online) onlineCount += 1
val bio = Util.decode(p(index + 2))
friendsModel.addElement(PersonItem(name, bio, online, "FRIEND"))
index += 3
i += 1
}
onlineSummary.setText(" FRIENDS ONLINE: " + onlineCount + "/" + count + " ")
}
// ----------------------------------------------------
// FRIEND REQUESTS
// ----------------------------------------------------
case "FRIEND_REQUESTS" =>
requestModel.clear()
if (p.length >= 2) {
val count = try p(1).toInt catch { case _: Throwable => 0 }
var index = 2
var i = 0
while (i < count && index + 2 < p.length) {
val name = Util.decode(p(index))
val online = p(index + 1) == "1"
val bio = Util.decode(p(index + 2))
requestModel.addElement(PersonItem(name, bio, online, "INCOMING"))
index += 3
i += 1
}
requestsButton.setText(
if (count > 0) "FRIEND REQUESTS (" + count + ")" else "FRIEND REQUESTS"
)
if (count > 0) {
val choices = new Array[String](requestModel.size())
var k = 0
while (k < requestModel.size()) {
choices(k) = requestModel.getElementAt(k).username
k += 1
}
val choiceObjects = choices.map(_.asInstanceOf[Object])
val selected = JOptionPane.showInputDialog(
frame,
"Pending friend requests. Select a person:",
"FRIEND REQUESTS",
JOptionPane.QUESTION_MESSAGE,
null,
choiceObjects,
choices(0)
)
if (selected != null) {
val action = JOptionPane.showOptionDialog(
frame,
"Friend request from " + selected,
"FRIEND REQUEST",
JOptionPane.DEFAULT_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
Array[AnyRef]("ACCEPT", "REJECT", "CANCEL"),
"ACCEPT"
)
if (action == 0) {
client.send("FRIEND_ACCEPT|" + Util.encode(selected.toString))
} else if (action == 1) {
client.send("FRIEND_REJECT|" + Util.encode(selected.toString))
}
}
}
}
// ----------------------------------------------------
// FRIEND EVENTS
// ----------------------------------------------------
case "FRIEND_REQUEST_SENT" =>
searchPeople()
case "FRIEND_REQUEST_RECEIVED" =>
val from = if (p.length >= 2) Util.decode(p(1)) else "Someone"
JOptionPane.showMessageDialog(
frame,
from + " sent you a friend request. Open FRIEND REQUESTS to accept or reject.",
APP_NAME,
JOptionPane.INFORMATION_MESSAGE
)
client.send("FRIEND_REQUESTS")
case "FRIEND_ACCEPTED" =>
val accepted = if (p.length >= 2) Util.decode(p(1)) else "Friend"
JOptionPane.showMessageDialog(
frame,
"You are now friends with " + accepted + ". The private chat is ready.",
APP_NAME,
JOptionPane.INFORMATION_MESSAGE
)
refreshFriends()
searchPeople()
case "FRIEND_REQUEST_REJECTED" =>
refreshFriends()
searchPeople()
case "FRIEND_REMOVED" =>
refreshFriends()
searchPeople()
case "BLOCK_STATE" =>
val blockedName = if (p.length >= 2) Util.decode(p(1)) else "User"
val blocked = p.length >= 3 && p(2) == "1"
blockButton.setText(if (blocked) "UNBLOCK USER" else "BLOCK USER")
if (blocked) {
JOptionPane.showMessageDialog(frame, blockedName + " is blocked.", APP_NAME, JOptionPane.INFORMATION_MESSAGE)
}
refreshFriends()
searchPeople()
if (selectedPerson.equalsIgnoreCase(blockedName)) {
client.send("PROFILE|" + Util.encode(selectedPerson))
}
case "BLOCKED_BY" =>
val blocker = if (p.length >= 2) Util.decode(p(1)) else "A user"
JOptionPane.showMessageDialog(frame, blocker + " blocked you.", APP_NAME, JOptionPane.WARNING_MESSAGE)
refreshFriends()
searchPeople()
case "UNBLOCKED_BY" =>
refreshFriends()
searchPeople()
case "CLIENT_DISCONNECTED" =>
onlineSummary.setText(" CONNECTION LOST ")
typingLabel.setText("")
JOptionPane.showMessageDialog(
frame,
"The connection to the host server was lost. Restart/login again to reconnect.",
APP_NAME,
JOptionPane.WARNING_MESSAGE
)
// ----------------------------------------------------
// GROUP
// ----------------------------------------------------
case "GROUP_CREATED" =>
if (
p.length >= 2
) {
JOptionPane.showMessageDialog(
frame,
"Group created:\n" +
Util.decode(
p(1)
),
APP_NAME,
JOptionPane.INFORMATION_MESSAGE
)
}
case _ =>
}
}
// ========================================================
// FRIEND BUTTON STATE
// ========================================================
private def updateFriendButtons(
relation: String
): Unit = {
friendButton.setEnabled(
relation == "NONE"
)
acceptButton.setEnabled(
relation == "INCOMING"
)
rejectButton.setEnabled(
relation == "INCOMING"
)
removeFriendButton.setEnabled(
relation == "FRIEND"
)
openChatButton.setEnabled(
relation == "FRIEND"
)
blockButton.setEnabled(
selectedPerson.nonEmpty && relation != ""
)
blockButton.setText(
if (relation == "BLOCKED") "UNBLOCK USER" else "BLOCK USER"
)
}
// ========================================================
// DISPLAY AVATAR
// ========================================================
private def displayAvatar(
base64: String
): Unit = {
if (
base64 == null ||
base64.isEmpty
) {
profileImage.setIcon(
null
)
profileImage.setText(
"No Photo"
)
} else {
val image =
Util.base64ToImage(
base64
)
if (
image == null
) {
profileImage.setIcon(
null
)
profileImage.setText(
"No Photo"
)
} else {
val scaled =
image.getScaledInstance(
225,
165,
Image.SCALE_SMOOTH
)
profileImage.setText(
""
)
profileImage.setIcon(
new javax.swing.ImageIcon(
scaled
)
)
}
}
}
}
// ==========================================================
// START SCREEN
// ==========================================================
object StartScreen {
def show(): Unit = {
Util.setupFont()
val frame =
new JFrame(
APP_NAME
)
frame.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE
)
frame.setSize(
720,
520
)
frame.setLocationRelativeTo(
null
)
val root =
new JPanel(
new BorderLayout(
15,
15
)
)
root.setBorder(
BorderFactory.createEmptyBorder(
30,
30,
30,
30
)
)
val title =
new JLabel(
"<html>" +
"<center>" +
"<font size='7'><b>YADNESH LIVE CHAT</b></font><br>" +
"<font size='4'>PERMANENT ACCOUNT ? WIFI LIVE CHAT</font>" +
"</center>" +
"</html>",
SwingConstants.CENTER
)
root.add(
title,
BorderLayout.NORTH
)
val info =
new JTextArea(
"FEATURES\n\n" +
"? Permanent user accounts\n" +
"? Same account after restarting app\n" +
"? Same account available from another PC through the same server\n" +
"? PC to PC live Wi-Fi chat\n" +
"? 2-person direct chat\n" +
"? Multiple-person group chat\n" +
"? Username search\n" +
"? Online / offline status\n" +
"? Profile photo\n" +
"? CHOOSE IMAGE\n" +
"? PASTE IMAGE FROM CLIPBOARD\n" +
"? Public profile bio\n" +
"? Forgot Password recovery\n" +
"? Live typing indicator\n" +
"? Unread message counters\n" +
"? Block / unblock users\n" +
"? Logout + connection heartbeat\n" +
"? Export current chat to TXT\n\n" +
"Account files are stored on the HOST SERVER PC."
)
info.setEditable(
false
)
info.setLineWrap(
true
)
info.setWrapStyleWord(
true
)
root.add(
new JScrollPane(
info
),
BorderLayout.CENTER
)
val start =
new JButton(
"START YADNESH LIVE CHAT"
)
start.setPreferredSize(
new Dimension(
330,
55
)
)
root.add(
start,
BorderLayout.SOUTH
)
start.addActionListener(
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
frame.dispose()
val login =
new LoginFrame()
login.show()
}
}
)
frame.setContentPane(
root
)
frame.setVisible(
true
)
}
}
// ==========================================================
// LAUNCHER
// ==========================================================
def startApp(): Unit = {
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
try {
UIManager.setLookAndFeel(
UIManager
.getSystemLookAndFeelClassName
)
} catch {
case _: Throwable =>
}
StartScreen.show()
}
}
)
}
}
import javax.swing._
import java.awt._
import java.awt.event._
import java.awt.image.BufferedImage
import java.awt.image.DataBufferInt
import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable
/*
* OLD HOUSE 3D - improved standalone Scala/Swing prototype.
* Features: triangle rasterizer + depth buffer, near-plane clipping,
* hinged opening doors, stairs with changing player height, upstairs
* gallery, WASD movement, mouse look, flashlight, key, wardrobe hiding,
* noise attraction, enemy patrol/chase, and 5-day loop.
*
* This is an original low-poly prototype, not the original Granny game.
* It uses no third-party game-engine libraries, but it is not a
* commercial-quality full 3D engine.
*/
object OldHouse3DImproved {
case class V3(x: Double, y: Double, z: Double)
case class CV(x: Double, y: Double, z: Double)
case class Tri(a: V3, b: V3, c: V3, color: Color)
case class Box(x: Double, y: Double, z: Double,
sx: Double, sy: Double, sz: Double,
color: Color, solid: Boolean)
final class Door(val x: Double, val z: Double, val width: Double,
val plane: String, val hingeSide: Double,
val yBase: Double, val height: Double,
val locked: Boolean, val isExit: Boolean,
val name: String) {
var angle = 0.0
var target = 0.0
}
val SW = 1000
val SH = 620
val NEAR = 0.12
val FOCAL = 760.0
val WALL = new Color(86, 75, 64)
val WALL_ALT = new Color(110, 91, 72)
val FLOOR = new Color(76, 60, 46)
val WOOD = new Color(91, 57, 36)
val DARK_WOOD = new Color(46, 31, 24)
val triangles = ArrayBuffer[Tri]()
val solids = ArrayBuffer[Box]()
val doors = ArrayBuffer[Door]()
val keys = mutable.HashSet[Int]()
var px = 3.2
var pz = 4.15
var yaw = math.Pi
var pitch = 0.0
var onUpperFloor = false
var wakeProgress = 0.0
var started = false
var gameOver = false
var escaped = false
var hidden = false
var hidePlace = ""
var hasKey = false
var hasBomb = false
var bombThrown = false
var bombX = 0.0
var bombZ = 0.0
var bombY = 0.25
var bombDX = 0.0
var bombDZ = 0.0
var bombFuse = 0.0
var bombBlastTimer = 0.0
var enemyStunnedTimer = 0.0
var secretUnlocked = false
var secretLootTaken = false
var flashlight = true
var showMap = false
var day = 1
val keyX = 9.35
val keyZ = 3.35
val wardrobeX = 1.25
val wardrobeZ = 4.7
val bedX = 2.3
val bedZ = 2.1
val vaseX = 4.65
val vaseZ = 2.35
// Discoverable shelf and the hidden room behind its secret panel.
val clueShelfX = 3.55
val clueShelfZ = 10.55
val secretShelfX = 3.15
val secretShelfZ = 14.65
val bathroomDoorX = 18.65
val bathroomDoorZ = 6.0
val ladderX = 14.80
val ladderZ = 14.00
var vaseY = 0.9
var vaseVel = 0.0
var vaseFalling = false
var vaseBroken = false
var noiseX = vaseX
var noiseZ = vaseZ
var noiseTimer = 0.0
// Original pursuer character: not a copied Granny model.
var enemyX = 9.4
var enemyZ = 8.3
var enemyUpper = false
var enemyTargetX = enemyX
var enemyTargetZ = enemyZ
var aiTimer = 0.0
var patrolIndex = 0
val patrol = Array((9.4, 8.3), (10.3, 10.0), (9.2, 14.0),
(15.8, 8.0), (19.0, 9.0))
var message = "DAY 1 - PRESS ENTER TO WAKE UP"
var messageTimer = 5.0
var lastMouseX = -1
var lastMouseY = -1
var gameTimer: Timer = _
var frame: JFrame = _
var panel: GamePanel = _
var image: BufferedImage = _
var pixels: Array[Int] = _
var zBuffer: Array[Double] = _
def clamp(v: Double, lo: Double, hi: Double): Double =
math.max(lo, math.min(hi, v))
def dist(x1: Double, z1: Double, x2: Double, z2: Double): Double = {
val dx = x1 - x2
val dz = z1 - z2
math.sqrt(dx * dx + dz * dz)
}
def shade(c: Color, f0: Double): Color = {
val f = clamp(f0, 0.0, 1.35)
new Color(
clamp(c.getRed * f, 0, 255).toInt,
clamp(c.getGreen * f, 0, 255).toInt,
clamp(c.getBlue * f, 0, 255).toInt
)
}
// -------------------------- Geometry --------------------------
def addBox(x: Double, y: Double, z: Double,
sx: Double, sy: Double, sz: Double,
color: Color, solid: Boolean = true): Unit = {
val b = Box(x, y, z, sx, sy, sz, color, solid)
triangles ++= boxTriangles(b)
if (solid) solids += b
}
def boxTriangles(b: Box): ArrayBuffer[Tri] = {
val x0 = b.x - b.sx / 2; val x1 = b.x + b.sx / 2
val y0 = b.y - b.sy / 2; val y1 = b.y + b.sy / 2
val z0 = b.z - b.sz / 2; val z1 = b.z + b.sz / 2
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,3,2,1), Array(4,5,6,7), Array(0,4,7,3),
Array(1,2,6,5), Array(3,7,6,2), Array(0,1,5,4)
)
val factors = Array(0.68, 0.95, 0.58, 0.78, 1.22, 0.40)
val out = ArrayBuffer[Tri]()
for (i <- faces.indices) {
val f = faces(i)
val c = shade(b.color, factors(i))
out += Tri(v(f(0)), v(f(1)), v(f(2)), c)
out += Tri(v(f(0)), v(f(2)), v(f(3)), c)
}
out
}
def addWallX(x: Double, z0: Double, z1: Double,
gaps: Seq[(Double, Double)], yBase: Double = 0.0,
height: Double = 2.8, color: Color = WALL): Unit = {
var cursor = z0
for ((a,b) <- gaps.sortBy(_._1)) {
if (a > cursor + 0.001)
addBox(x, yBase + height/2, (cursor+a)/2,
0.22, height, a-cursor, color, true)
cursor = math.max(cursor,b)
}
if (cursor < z1 - 0.001)
addBox(x, yBase + height/2, (cursor+z1)/2,
0.22, height, z1-cursor, color, true)
}
def addWallZ(z: Double, x0: Double, x1: Double,
gaps: Seq[(Double, Double)], yBase: Double = 0.0,
height: Double = 2.8, color: Color = WALL): Unit = {
var cursor = x0
for ((a,b) <- gaps.sortBy(_._1)) {
if (a > cursor + 0.001)
addBox((cursor+a)/2, yBase + height/2, z,
a-cursor, height, 0.22, color, true)
cursor = math.max(cursor,b)
}
if (cursor < x1 - 0.001)
addBox((cursor+x1)/2, yBase + height/2, z,
x1-cursor, height, 0.22, color, true)
}
def addDoor(x: Double, z: Double, width: Double, plane: String,
hingeSide: Double = 1.0, yBase: Double = 0.0,
height: Double = 2.65, locked: Boolean = false,
isExit: Boolean = false, name: String = "Door"): Unit = {
doors += new Door(x,z,width,plane,hingeSide,yBase,height,locked,isExit,name)
}
def makeDoorTriangles(d: Door): ArrayBuffer[Tri] = {
val c = math.cos(d.angle)
val s = math.sin(d.angle)
// Local width and normal before rotating around the hinge.
val (baseUx, baseUz, baseNx, baseNz, pivotX, pivotZ) =
if (d.plane == "X") {
(0.0, d.hingeSide, d.hingeSide, 0.0,
d.x, d.z - d.hingeSide*d.width/2)
} else {
(d.hingeSide, 0.0, 0.0, 1.0,
d.x - d.hingeSide*d.width/2, d.z)
}
def rotateX(ux: Double, uz: Double): Double = ux*c + uz*s
def rotateZ(ux: Double, uz: Double): Double = uz*c - ux*s
val ux = rotateX(baseUx, baseUz)
val uz = rotateZ(baseUx, baseUz)
val nx = rotateX(baseNx, baseNz)
val nz = rotateZ(baseNx, baseNz)
val thick = 0.12
def point(u: Double, v: Double, y: Double): V3 =
V3(pivotX + ux*u + nx*v, y, pivotZ + uz*u + nz*v)
val v = Array(
point(0,-thick/2,d.yBase), point(d.width,-thick/2,d.yBase),
point(d.width,-thick/2,d.yBase+d.height), point(0,-thick/2,d.yBase+d.height),
point(0,thick/2,d.yBase), point(d.width,thick/2,d.yBase),
point(d.width,thick/2,d.yBase+d.height), point(0,thick/2,d.yBase+d.height)
)
val faces = Array(
Array(0,3,2,1), Array(4,5,6,7), Array(0,4,7,3),
Array(1,2,6,5), Array(3,7,6,2), Array(0,1,5,4)
)
val out = ArrayBuffer[Tri]()
val factors = Array(0.72, 0.88, 0.56, 0.92, 1.12, 0.42)
for (i <- faces.indices) {
val f = faces(i)
val col = shade(if (d.isExit) new Color(75,40,27) else new Color(94,59,37), factors(i))
out += Tri(v(f(0)),v(f(1)),v(f(2)),col)
out += Tri(v(f(0)),v(f(2)),v(f(3)),col)
}
// Small handle on the front side.
val handle = Box(
pivotX + ux*d.width*0.82 + nx*0.09,
d.yBase + d.height*0.48,
pivotZ + uz*d.width*0.82 + nz*0.09,
0.08, 0.08, 0.08, new Color(211,174,73), false
)
out ++= boxTriangles(handle)
out
}
def buildHouse(): Unit = {
triangles.clear(); solids.clear(); doors.clear()
// Main ground-floor slab.
addBox(12.0,-0.16,9.0,24.0,0.32,18.0,FLOOR,false)
// Exterior walls, with a front exit opening.
addBox(0.12,1.4,9.0,0.24,2.8,18.0,WALL,true)
addBox(23.88,1.4,9.0,0.24,2.8,18.0,WALL,true)
addWallZ(0.12,0.12,23.88,Seq(),0.0,2.8,WALL_ALT)
addWallZ(17.88,0.12,23.88,Seq((19.7,21.3)),0.0,2.8,WALL_ALT)
// Ground-floor partitions. Door openings are real gaps in the wall.
addWallX(6.0,0.25,17.75,Seq((2.4,3.6),(8.2,9.4)))
addWallX(12.0,0.25,17.75,Seq((3.0,4.2),(8.0,9.2),(14.6,15.8)))
// The broad opening to the right leads into the bathroom wing.
addWallZ(6.0,0.25,23.75,Seq((3.5,4.8),(8.0,9.2),(14.3,17.3),(18.0,19.3)))
// The x=4.0..5.2 gap is hidden behind a hinged secret panel.
addWallZ(12.0,0.25,23.75,Seq((4.0,5.2),(8.5,9.7),(15.5,16.7),(19.9,21.3)))
addDoor(6.0,3.0,1.2,"X",1.0,0.0,2.65,false,false,"Bedroom door")
addDoor(6.0,8.8,1.2,"X",-1.0,0.0,2.65,false,false,"Hall door")
addDoor(12.0,3.6,1.2,"X",-1.0,0.0,2.65,false,false,"Kitchen door")
addDoor(12.0,8.6,1.2,"X",1.0,0.0,2.65,false,false,"Stair hall door")
addDoor(12.0,15.2,1.2,"X",-1.0,0.0,2.65,false,false,"Back room door")
addDoor(4.15,6.0,1.3,"Z",1.0,0.0,2.65,false,false,"Room door")
addDoor(8.6,6.0,1.2,"Z",-1.0,0.0,2.65,false,false,"Hall door")
addDoor(9.1,12.0,1.2,"Z",-1.0,0.0,2.65,false,false,"Back hall door")
addDoor(16.1,12.0,1.2,"Z",1.0,0.0,2.65,false,false,"Back room door")
addDoor(20.6,12.0,1.2,"Z",-1.0,0.0,2.65,false,false,"Exit hall door")
addDoor(20.5,17.88,1.6,"Z",1.0,0.0,2.65,true,true,"Locked exit")
addDoor(bathroomDoorX,bathroomDoorZ,1.3,"Z",1.0,0.0,2.65,false,false,"Bathroom door")
addDoor(4.6,12.0,1.2,"Z",1.0,0.0,2.65,false,false,"Hidden shelf panel")
// Bedroom furniture.
addBox(2.3,0.23,2.1,2.25,0.34,2.9,DARK_WOOD,true)
addBox(2.3,0.48,2.1,2.05,0.18,2.68,new Color(115,106,94),true)
addBox(2.3,0.60,1.02,1.08,0.12,0.48,new Color(194,184,166),true)
for (x <- Seq(1.35,3.25); z <- Seq(0.85,3.25))
addBox(x,0.13,z,0.17,0.26,0.17,DARK_WOOD,true)
// Wardrobe.
addBox(wardrobeX,1.08,wardrobeZ,0.92,2.16,0.86,new Color(61,41,30),true)
addBox(1.73,1.08,wardrobeZ,0.08,2.03,0.82,new Color(83,55,39),true)
// Tables, cupboards and other simple room props.
addBox(4.75,0.42,2.3,0.82,0.82,0.82,WOOD,true)
addBox(4.75,0.86,2.3,0.88,0.08,0.88,DARK_WOOD,true)
addBox(9.0,0.65,5.0,1.65,0.14,1.2,WOOD,true)
for (x <- Seq(8.35,9.65); z <- Seq(4.55,5.45))
addBox(x,0.30,z,0.13,0.60,0.13,DARK_WOOD,true)
addBox(15.0,0.90,2.0,1.0,1.8,0.65,DARK_WOOD,true)
addBox(18.6,0.90,8.6,1.05,1.8,0.65,WOOD,true)
addBox(5.15,0.72,16.45,0.62,1.44,0.58,DARK_WOOD,true)
addBox(18.5,0.38,14.7,1.3,0.72,0.8,DARK_WOOD,true)
// Discoverable shelf in the back hall. Interact with it to reveal the panel.
addBox(clueShelfX,0.95,clueShelfZ,1.05,1.9,0.36,DARK_WOOD,true)
addBox(clueShelfX,0.42,clueShelfZ,1.18,0.10,0.48,WOOD,false)
addBox(clueShelfX,1.00,clueShelfZ,1.18,0.10,0.48,WOOD,false)
addBox(clueShelfX,1.58,clueShelfZ,1.18,0.10,0.48,WOOD,false)
addBox(clueShelfX,2.08,clueShelfZ,1.18,0.10,0.48,WOOD,false)
// Secret storage room shelf, behind the panel.
addBox(secretShelfX,1.02,secretShelfZ,1.10,2.04,0.40,DARK_WOOD,true)
addBox(secretShelfX,0.42,secretShelfZ,1.22,0.10,0.48,WOOD,false)
addBox(secretShelfX,1.00,secretShelfZ,1.22,0.10,0.48,WOOD,false)
addBox(secretShelfX,1.58,secretShelfZ,1.22,0.10,0.48,WOOD,false)
addBox(secretShelfX,2.08,secretShelfZ,1.22,0.10,0.48,WOOD,false)
// Bathroom fixtures in their own room: bathtub, toilet, sink and mirror.
addBox(21.85,0.30,4.70,2.35,0.55,1.05,new Color(147,145,132),true)
addBox(21.85,0.58,4.70,2.02,0.12,0.78,new Color(185,183,171),false)
addBox(20.05,0.27,2.45,0.72,0.42,0.82,new Color(185,181,168),true)
addBox(20.05,0.56,2.58,0.55,0.22,0.48,new Color(205,202,189),false)
addBox(22.85,0.48,1.55,0.80,0.95,0.60,new Color(105,103,96),true)
addBox(22.85,1.02,1.55,0.90,0.12,0.68,new Color(186,183,171),false)
addBox(22.15,1.95,0.28,1.00,0.82,0.08,new Color(42,57,61),false)
// Decorative climbable ladder connecting the lower hall to the upper wing.
addBox(ladderX-0.24,1.50,ladderZ,0.07,2.95,0.08,DARK_WOOD,false)
addBox(ladderX+0.24,1.50,ladderZ,0.07,2.95,0.08,DARK_WOOD,false)
for (i <- 0 until 10) {
addBox(ladderX,0.18+i*0.29,ladderZ,0.52,0.065,0.08,WOOD,false)
}
// Stairs: 10 actual polygonal steps; the collision floor follows the ramp.
for (i <- 0 until 10) {
val h = (i+1)*0.30
val zc = 6.5 + (i+0.5)*0.40
addBox(15.8,h/2,zc,3.0,h,0.42,new Color(87,63,46),false)
}
// Solid side walls keep the player on the stairs.
addBox(14.20,1.45,8.5,0.18,2.9,4.2,DARK_WOOD,true)
addBox(17.40,1.45,8.5,0.18,2.9,4.2,DARK_WOOD,true)
// Ground floor wall prevents walking through the underside of the upper wing.
addWallZ(10.5,17.45,23.75,Seq(),0.0,2.8,WALL_ALT)
// Upper floor platform/wing. Top is y = 3.0.
addBox(19.0,2.90,14.05,9.4,0.20,7.10,new Color(71,56,43),false)
// Upper-level walls.
addWallX(14.30,10.55,17.62,Seq((13.35,14.65)),3.0,2.8,WALL_ALT)
addWallX(23.75,10.50,17.65,Seq(),3.0,2.8,WALL_ALT)
addWallZ(10.50,17.35,23.75,Seq(),3.0,2.8,WALL_ALT)
addWallZ(17.62,14.30,23.75,Seq(),3.0,2.8,WALL_ALT)
addWallX(18.50,10.55,17.60,Seq((12.75,14.05)),3.0,2.8,WALL)
addWallZ(14.00,18.50,23.70,Seq((20.0,21.3)),3.0,2.8,WALL)
addDoor(14.30,14.0,1.3,"X",1.0,3.0,2.65,false,false,"Upper door")
addDoor(18.50,13.4,1.3,"X",-1.0,3.0,2.65,false,false,"Upper hall door")
addDoor(20.65,14.0,1.3,"Z",1.0,3.0,2.65,false,false,"Upper room door")
// Upper-floor furniture at y=3.
addBox(20.8,3.35,12.1,1.5,0.7,0.9,WOOD,true)
addBox(22.1,3.55,15.5,1.0,1.1,0.7,DARK_WOOD,true)
// Wall windows: dark glass plus pale frames.
addBox(3.0,2.0,0.24,1.4,0.8,0.08,new Color(28,43,51),false)
addBox(14.0,2.0,0.24,1.4,0.8,0.08,new Color(28,43,51),false)
addBox(10.0,2.0,0.24,0.62,0.72,0.08,new Color(52,41,31),false)
}
// ------------------------ Floor & collision ------------------------
def inStair(x: Double, z: Double): Boolean =
x >= 14.30 && x <= 17.30 && z >= 6.50 && z <= 10.50
def floorY(x: Double, z: Double, upper: Boolean): Double = {
if (inStair(x,z)) 3.0 * clamp((z-6.50)/4.0,0.0,1.0)
else if (upper) 3.0
else 0.0
}
def updateLevel(upper: Boolean, x: Double, z: Double): Boolean = {
if (inStair(x,z)) {
if (z >= 10.46) true
else if (z <= 6.54) false
else upper
} else upper
}
def boxBlocks(b: Box, x: Double, z: Double, upper: Boolean): Boolean = {
val feet = floorY(x,z,upper)
val bodyBottom = feet + 0.05
val bodyTop = feet + 1.65
val bBottom = b.y - b.sy/2
val bTop = b.y + b.sy/2
val vertical = bodyTop > bBottom && bodyBottom < bTop
vertical && math.abs(x-b.x) < b.sx/2 + 0.19 &&
math.abs(z-b.z) < b.sz/2 + 0.19
}
def doorBlocks(d: Door, x: Double, z: Double, upper: Boolean): Boolean = {
val feet = floorY(x,z,upper)
val overlapsY = feet + 1.65 > d.yBase && feet < d.yBase + d.height
if (!overlapsY || d.angle > 0.92) false
else if (d.plane == "X")
math.abs(x-d.x) < 0.34 && math.abs(z-d.z) < d.width/2 + 0.20
else
math.abs(z-d.z) < 0.34 && math.abs(x-d.x) < d.width/2 + 0.20
}
def blocked(x: Double, z: Double, upper: Boolean): Boolean = {
if (x < 0.35 || x > 23.65 || z < 0.35) return true
if (z > 17.95) {
val exitPassage = doors.exists(d => d.isExit && d.angle > 1.0 &&
x > 19.35 && x < 21.65 && z < 19.1)
if (!exitPassage) return true
}
solids.exists(b => boxBlocks(b,x,z,upper)) || doors.exists(d => doorBlocks(d,x,z,upper))
}
def movePlayer(dx: Double, dz: Double): Unit = {
val nx = px + dx
val levelX = updateLevel(onUpperFloor,nx,pz)
if (!blocked(nx,pz,levelX)) { px = nx; onUpperFloor = levelX }
val nz = pz + dz
val levelZ = updateLevel(onUpperFloor,px,nz)
if (!blocked(px,nz,levelZ)) { pz = nz; onUpperFloor = levelZ }
}
// ----------------------------- Gameplay -----------------------------
def say(s: String, seconds: Double = 2.8): Unit = {
message = s
messageTimer = seconds
}
def nearestDoor(): Option[Door] = {
val feet = floorY(px,pz,onUpperFloor)
val nearby = doors.filter(d =>
dist(px,pz,d.x,d.z) < 1.75 &&
feet + 1.65 > d.yBase && feet < d.yBase + d.height
)
if (nearby.isEmpty) None else Some(nearby.minBy(d => dist(px,pz,d.x,d.z)))
}
def interact(): Unit = {
if (!started || gameOver || escaped) return
if (hidden) {
hidden = false
say("YOU LEFT " + (if (hidePlace.isEmpty) "YOUR HIDING PLACE" else hidePlace) + ".")
hidePlace = ""
return
}
if (dist(px,pz,wardrobeX,wardrobeZ) < 1.25 && !onUpperFloor) {
hidden = true
hidePlace = "WARDROBE"
say("HIDDEN IN THE WARDROBE. PRESS E TO LEAVE.")
return
}
if (dist(px,pz,bedX,bedZ) < 1.65 && !onUpperFloor) {
hidden = true
hidePlace = "UNDER THE BED"
say("HIDING UNDER THE BED. PRESS E TO CRAWL OUT.")
return
}
if (dist(px,pz,ladderX,ladderZ) < 1.20) {
onUpperFloor = !onUpperFloor
say(if (onUpperFloor) "YOU CLIMBED THE LADDER UPSTAIRS." else "YOU CLIMBED THE LADDER DOWNSTAIRS.",3.0)
return
}
if (dist(px,pz,clueShelfX,clueShelfZ) < 1.45 && !secretUnlocked && !onUpperFloor) {
secretUnlocked = true
doors.find(_.name == "Hidden shelf panel").foreach(_.target = 1.35)
say("SECRET PANEL FOUND! A HIDDEN ROOM IS OPEN.",4.0)
try { Toolkit.getDefaultToolkit.beep() } catch { case _: Throwable => () }
return
}
if (secretUnlocked && dist(px,pz,secretShelfX,secretShelfZ) < 1.45 && !onUpperFloor) {
if (!secretLootTaken) {
secretLootTaken = true
hasBomb = true
say("SECRET SHELF: STUN BOMB COLLECTED. PRESS Q TO THROW IT.",4.0)
} else {
say("THE SECRET SHELF IS EMPTY.")
}
return
}
if (dist(px,pz,vaseX,vaseZ) < 1.25 && !vaseFalling && !vaseBroken && !onUpperFloor) {
vaseFalling = true
vaseVel = 0.0
say("THE VASE IS FALLING...",1.5)
try { Toolkit.getDefaultToolkit.beep() } catch { case _: Throwable => () }
return
}
nearestDoor() match {
case Some(d) =>
if (d.name == "Hidden shelf panel" && !secretUnlocked) {
say("THE PANEL HAS NO HANDLE. SEARCH THE SHELF NEARBY.")
} else if (d.locked && !hasKey) {
say("LOCKED. FIND THE KEY FIRST!")
} else {
d.target = if (d.target > 0.5) 0.0 else 1.35
if (d.isExit && d.target > 0.5) say("EXIT DOOR OPENING...")
else say(if (d.target > 0.5) d.name.toUpperCase + " OPENING" else d.name.toUpperCase + " CLOSING")
}
case None => say("NOTHING TO INTERACT WITH HERE.")
}
}
def resetDay(): Unit = {
day += 1
if (day > 5) {
gameOver = true
say("DAY 5 FINISHED - GAME OVER",20.0)
} else {
px = 3.2; pz = 4.15; yaw = math.Pi; pitch = 0.0
onUpperFloor = false; hidden = false; hidePlace = ""; hasKey = false
hasBomb = false; bombThrown = false; bombFuse = 0.0; bombBlastTimer = 0.0
enemyStunnedTimer = 0.0; secretUnlocked = false; secretLootTaken = false
doors.foreach(d => { d.angle = 0.0; d.target = 0.0 })
enemyX = 9.4; enemyZ = 8.3; enemyUpper = false
enemyTargetX = enemyX; enemyTargetZ = enemyZ
vaseY = 0.9; vaseVel = 0.0; vaseFalling = false; vaseBroken = false
noiseTimer = 0.0; wakeProgress = 0.0
say("DAY " + day + " - YOU WOKE UP AGAIN!",4.0)
started = true
}
}
def resetGame(): Unit = {
px = 3.2; pz = 4.15; yaw = math.Pi; pitch = 0.0
onUpperFloor = false; wakeProgress = 0.0
started = false; gameOver = false; escaped = false; hidden = false; hidePlace = ""
hasKey = false; hasBomb = false; bombThrown = false; bombFuse = 0.0; bombBlastTimer = 0.0
enemyStunnedTimer = 0.0; secretUnlocked = false; secretLootTaken = false
flashlight = true; showMap = false; day = 1
doors.foreach(d => { d.angle = 0.0; d.target = 0.0 })
vaseY = 0.9; vaseVel = 0.0; vaseFalling = false; vaseBroken = false
noiseTimer = 0.0; enemyX = 9.4; enemyZ = 8.3; enemyUpper = false
enemyTargetX = enemyX; enemyTargetZ = enemyZ; aiTimer = 0.0; patrolIndex = 0
message = "DAY 1 - PRESS ENTER TO WAKE UP"; messageTimer = 5.0
}
def wake(): Unit = {
started = true
wakeProgress = 0.0
say("DAY 1. GET OUT OF THE BEDROOM.",4.0)
}
def playerEyeY: Double = floorY(px,pz,onUpperFloor) + 0.55 + 1.05*wakeProgress
def updateDoors(dt: Double): Unit = {
doors.foreach { d =>
val speed = 1.9*dt
if (d.angle < d.target) d.angle = math.min(d.target,d.angle+speed)
else if (d.angle > d.target) d.angle = math.max(d.target,d.angle-speed)
}
}
def walkableGrid(x: Double, z: Double, upper: Boolean): Boolean = !blocked(x,z,upper)
def gridPathStep(sx: Double, sz: Double, tx: Double, tz: Double,
upper: Boolean): (Double,Double) = {
val cell = 0.8
val w = 30; val h = 23
def gx(x: Double) = clamp((x/cell).toInt,0,w-1).toInt
def gz(z: Double) = clamp((z/cell).toInt,0,h-1).toInt
val start = gz(sz)*w + gx(sx)
val goal = gz(tz)*w + gx(tx)
if (start == goal) return (tx,tz)
val prev = Array.fill(w*h)(-1)
val q = mutable.Queue[Int]()
prev(start) = start; q.enqueue(start)
val ds = Array((1,0),(-1,0),(0,1),(0,-1))
var checks = 0
while (q.nonEmpty && prev(goal) == -1 && checks < w*h) {
val cur = q.dequeue()
val cx = cur%w; val cz = cur/w
for ((dx,dz) <- ds) {
val nx = cx+dx; val nz = cz+dz
if (nx>=0 && nz>=0 && nx<w && nz<h) {
val ni = nz*w+nx
if (prev(ni) == -1 && walkableGrid(nx*cell+cell/2,nz*cell+cell/2,upper)) {
prev(ni)=cur; q.enqueue(ni)
}
}
}
checks += 1
}
if (prev(goal) == -1) return (tx,tz)
var cur = goal
while (prev(cur) != start && cur != start && prev(cur) >= 0) cur = prev(cur)
((cur%w)*cell+cell/2,(cur/w)*cell+cell/2)
}
def throwStunBomb(): Unit = {
if (!started || gameOver || escaped || hidden) return
if (!hasBomb) {
say("NO STUN BOMB. SEARCH THE SECRET SHELF.")
return
}
if (bombThrown) {
say("A STUN BOMB IS ALREADY IN THE AIR.")
return
}
hasBomb = false
bombThrown = true
bombX = px + math.sin(yaw) * 0.55
bombZ = pz + math.cos(yaw) * 0.55
bombY = playerEyeY - 0.25
bombDX = math.sin(yaw)
bombDZ = math.cos(yaw)
bombFuse = 0.85
say("STUN BOMB THROWN! KEEP YOUR DISTANCE.",2.0)
}
def detonateStunBomb(): Unit = {
bombThrown = false
bombFuse = 0.0
bombBlastTimer = 1.25
noiseX = bombX
noiseZ = bombZ
noiseTimer = 7.0
val hit = enemyUpper == onUpperFloor && dist(bombX,bombZ,enemyX,enemyZ) < 3.2
if (hit) {
enemyStunnedTimer = 7.0
say("DIRECT HIT! THE PURSUER IS STUNNED.",3.0)
} else {
say("STUN BOMB BURST. THE SOUND DREW THE PURSUER.",3.0)
}
try { Toolkit.getDefaultToolkit.beep() } catch { case _: Throwable => () }
}
def updateEnemy(dt: Double): Unit = {
if (hidden || gameOver || escaped || enemyStunnedTimer > 0.0) return
val dPlayer = dist(px,pz,enemyX,enemyZ)
var tx = enemyX; var tz = enemyZ
var targetUpper = enemyUpper
if (noiseTimer > 0.0) {
// A ground-floor noise can draw the enemy down from the gallery.
if (enemyUpper && !inStair(enemyX,enemyZ)) { tx = 15.8; tz = 10.30; targetUpper = true }
else if (enemyUpper && inStair(enemyX,enemyZ)) { tx = 15.8; tz = 6.25; targetUpper = false }
else { tx = noiseX; tz = noiseZ; targetUpper = false }
} else if (dPlayer < 10.5) {
// The pursuer uses the stair opening to travel between the two levels.
if (onUpperFloor && !enemyUpper) {
if (enemyZ < 6.45 || enemyX < 14.3 || enemyX > 17.3) { tx = 15.8; tz = 6.70 }
else { tx = 15.8; tz = 10.75 }
targetUpper = false
} else if (!onUpperFloor && enemyUpper) {
if (enemyZ > 10.55 || enemyX < 14.3 || enemyX > 17.3) { tx = 15.8; tz = 10.30 }
else { tx = 15.8; tz = 6.25 }
targetUpper = true
} else {
tx = px; tz = pz; targetUpper = onUpperFloor
}
} else {
val p = patrol(patrolIndex)
tx = p._1; tz = p._2; targetUpper = false
if (dist(enemyX,enemyZ,tx,tz)<0.8) patrolIndex = (patrolIndex+1)%patrol.length
}
// The pursuer can push open ordinary doors when it reaches them.
doors.foreach { d =>
if (!d.locked && !d.isExit &&
(d.name != "Hidden shelf panel" || secretUnlocked) &&
dist(enemyX,enemyZ,d.x,d.z) < 1.10) {
d.target = 1.35
}
}
aiTimer += dt
if (aiTimer >= 0.55) {
aiTimer = 0.0
val step = gridPathStep(enemyX,enemyZ,tx,tz,enemyUpper)
enemyTargetX = step._1; enemyTargetZ = step._2
}
val dx = enemyTargetX-enemyX; val dz = enemyTargetZ-enemyZ
val len = math.sqrt(dx*dx+dz*dz)
if (len > 0.06) {
val step = math.min(len,0.95*dt)
val nx = enemyX + dx/len*step
val nz = enemyZ + dz/len*step
val nxLevel = updateLevel(enemyUpper,nx,enemyZ)
if (!blocked(nx,enemyZ,nxLevel)) { enemyX=nx; enemyUpper=nxLevel }
val nzLevel = updateLevel(enemyUpper,enemyX,nz)
if (!blocked(enemyX,nz,nzLevel)) { enemyZ=nz; enemyUpper=nzLevel }
}
if (!hidden && enemyUpper == onUpperFloor &&
dist(px,pz,enemyX,enemyZ)<0.62) resetDay()
}
def updateGame(): Unit = {
val dt = 0.033
updateDoors(dt)
if (!started || gameOver || escaped) return
if (messageTimer > 0.0) messageTimer -= dt
if (noiseTimer > 0.0) noiseTimer -= dt
if (enemyStunnedTimer > 0.0) enemyStunnedTimer = math.max(0.0,enemyStunnedTimer-dt)
if (bombBlastTimer > 0.0) bombBlastTimer = math.max(0.0,bombBlastTimer-dt)
wakeProgress = math.min(1.0,wakeProgress+dt*0.65)
if (bombThrown) {
val nx = bombX + bombDX * 5.2 * dt
val nz = bombZ + bombDZ * 5.2 * dt
if (blocked(nx,nz,onUpperFloor)) {
bombFuse = math.min(bombFuse,0.01)
} else {
bombX = nx
bombZ = nz
bombY = floorY(bombX,bombZ,onUpperFloor) + 0.24 + 0.08*math.sin((0.85-bombFuse)*10.0)
}
bombFuse -= dt
if (bombFuse <= 0.0) detonateStunBomb()
}
if (vaseFalling && !vaseBroken) {
vaseVel += 9.8*dt
vaseY -= vaseVel*dt
if (vaseY <= 0.12) {
vaseY=0.12; vaseBroken=true; vaseFalling=false
noiseX=vaseX; noiseZ=vaseZ; noiseTimer=12.0
say("CRASH! THE ENEMY HEARD THAT!",4.0)
}
}
if (!hidden) {
var forward=0.0; var side=0.0
if (keys.contains(KeyEvent.VK_W)) forward += 1.0
if (keys.contains(KeyEvent.VK_S)) forward -= 1.0
if (keys.contains(KeyEvent.VK_D)) side += 1.0
if (keys.contains(KeyEvent.VK_A)) side -= 1.0
val len=math.sqrt(forward*forward+side*side)
if (len>0.0) {
val speed=if (keys.contains(KeyEvent.VK_SHIFT)) 3.3 else 2.0
val f=forward/len*speed*dt; val s=side/len*speed*dt
val dx=math.sin(yaw)*f+math.cos(yaw)*s
val dz=math.cos(yaw)*f-math.sin(yaw)*s
movePlayer(dx,dz)
}
}
// Bumping the fragile vase also knocks it down; it need not be pressed with E.
if (!vaseBroken && !vaseFalling && !onUpperFloor && dist(px,pz,vaseX,vaseZ) < 0.68) {
vaseFalling = true
vaseVel = 0.0
say("YOU KNOCKED THE VASE OVER!",1.4)
}
if (!hasKey && !onUpperFloor && dist(px,pz,keyX,keyZ)<0.55) {
hasKey=true; say("KEY COLLECTED. FIND THE EXIT!",3.5)
}
val exit = doors.find(_.isExit)
if (exit.isDefined && exit.get.angle>1.0 && !onUpperFloor &&
px>19.4 && px<21.6 && pz>18.0) {
escaped=true; say("YOU ESCAPED THE OLD HOUSE!",20.0)
}
updateEnemy(dt)
}
// ---------------------- Camera & depth renderer ----------------------
def camera(v: V3): CV = {
val dx=v.x-px; val dy=v.y-playerEyeY; val dz=v.z-pz
val cx=dx*math.cos(yaw)-dz*math.sin(yaw)
val forward=dx*math.sin(yaw)+dz*math.cos(yaw)
val cp=math.cos(pitch); val sp=math.sin(pitch)
CV(cx,dy*cp-forward*sp,dy*sp+forward*cp)
}
def clipNear(poly0: Vector[CV]): Vector[CV] = {
val out=ArrayBuffer[CV]()
var i=0
while (i<poly0.length) {
val a=poly0(i); val b=poly0((i+1)%poly0.length)
val ia=a.z>=NEAR; val ib=b.z>=NEAR
if (ia && ib) out += b
else if (ia && !ib) {
val t=(NEAR-a.z)/(b.z-a.z)
out += CV(a.x+(b.x-a.x)*t,a.y+(b.y-a.y)*t,NEAR)
} else if (!ia && ib) {
val t=(NEAR-a.z)/(b.z-a.z)
out += CV(a.x+(b.x-a.x)*t,a.y+(b.y-a.y)*t,NEAR)
out += b
}
i+=1
}
out.toVector
}
def edge(ax: Double, ay: Double, bx: Double, by: Double,
px0: Double, py0: Double): Double =
(px0-ax)*(by-ay)-(py0-ay)*(bx-ax)
def rasterTriangle(a: CV,b: CV,c: CV,color: Color,w: Int,h: Int): Unit = {
val ax=w/2.0+a.x*FOCAL/a.z; val ay=h/2.0-a.y*FOCAL/a.z
val bx=w/2.0+b.x*FOCAL/b.z; val by=h/2.0-b.y*FOCAL/b.z
val cx=w/2.0+c.x*FOCAL/c.z; val cy=h/2.0-c.y*FOCAL/c.z
val area=edge(ax,ay,bx,by,cx,cy)
if (math.abs(area)<0.00001) return
val minX=math.max(0,math.floor(math.min(ax,math.min(bx,cx))).toInt)
val maxX=math.min(w-1,math.ceil(math.max(ax,math.max(bx,cx))).toInt)
val minY=math.max(0,math.floor(math.min(ay,math.min(by,cy))).toInt)
val maxY=math.min(h-1,math.ceil(math.max(ay,math.max(by,cy))).toInt)
if (minX>maxX || minY>maxY) return
val rgb=color.getRGB
val invA=1.0/a.z; val invB=1.0/b.z; val invC=1.0/c.z
var y=minY
while (y<=maxY) {
var x=minX
val py0=y+0.5
while (x<=maxX) {
val px0=x+0.5
val wa=edge(bx,by,cx,cy,px0,py0)/area
val wb=edge(cx,cy,ax,ay,px0,py0)/area
val wc=1.0-wa-wb
if (wa >= -0.00001 && wb >= -0.00001 && wc >= -0.00001) {
val q=wa*invA+wb*invB+wc*invC
val idx=y*w+x
if (q>zBuffer(idx)) { zBuffer(idx)=q; pixels(idx)=rgb }
}
x+=1
}
y+=1
}
}
def dynamicGeometry(): ArrayBuffer[Tri] = {
val out=ArrayBuffer[Tri]()
doors.foreach(d => out ++= makeDoorTriangles(d))
// Key is a small low-poly gold object.
if (!hasKey) {
out ++= boxTriangles(Box(keyX,0.20,keyZ,0.42,0.06,0.06,new Color(236,190,42),false))
out ++= boxTriangles(Box(keyX-0.22,0.20,keyZ,0.13,0.09,0.11,new Color(250,213,66),false))
}
// Thrown stun charge and brief blast flash.
if (bombThrown) {
out ++= boxTriangles(Box(bombX,bombY,bombZ,0.24,0.24,0.24,new Color(65,190,220),false))
out ++= boxTriangles(Box(bombX,bombY+0.10,bombZ,0.10,0.10,0.10,new Color(240,220,120),false))
}
if (bombBlastTimer > 0.0) {
val radius = 0.35 + (1.25-bombBlastTimer)*2.2
out ++= boxTriangles(Box(bombX,bombY,bombZ,radius,0.10,radius,new Color(85,185,220),false))
}
// Vase and broken fragments.
if (!vaseBroken) {
out ++= boxTriangles(Box(vaseX,vaseY,vaseZ,0.24,0.34,0.24,new Color(118,64,48),false))
out ++= boxTriangles(Box(vaseX,vaseY+0.17,vaseZ,0.16,0.05,0.16,new Color(155,100,68),false))
} else {
out ++= boxTriangles(Box(vaseX-0.12,0.06,vaseZ,0.18,0.05,0.12,new Color(118,64,48),false))
out ++= boxTriangles(Box(vaseX+0.13,0.05,vaseZ+0.04,0.14,0.04,0.09,new Color(155,100,68),false))
}
// Original simple low-poly pursuer (not the Granny character model).
val ey=if (enemyUpper) 3.0 else floorY(enemyX,enemyZ,false)
out ++= boxTriangles(Box(enemyX,ey+1.02,enemyZ,0.50,1.12,0.36,new Color(65,17,22),false))
out ++= boxTriangles(Box(enemyX,ey+1.78,enemyZ,0.42,0.43,0.42,new Color(174,151,127),false))
out ++= boxTriangles(Box(enemyX,ey+1.99,enemyZ,0.47,0.14,0.47,new Color(28,22,20),false))
out ++= boxTriangles(Box(enemyX-0.32,ey+1.04,enemyZ,0.17,0.82,0.19,new Color(94,61,47),false))
out ++= boxTriangles(Box(enemyX+0.32,ey+1.04,enemyZ,0.17,0.82,0.19,new Color(94,61,47),false))
out ++= boxTriangles(Box(enemyX-0.14,ey+0.31,enemyZ,0.19,0.60,0.22,new Color(35,28,27),false))
out ++= boxTriangles(Box(enemyX+0.14,ey+0.31,enemyZ,0.19,0.60,0.22,new Color(35,28,27),false))
out
}
def lightColor(t: Tri): Color = {
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
val dx=cx-px; val dz=cz-pz
val d=math.sqrt(dx*dx+dz*dz)
val dot=if (d<0.001) 1.0 else (dx*math.sin(yaw)+dz*math.cos(yaw))/d
val light=if (flashlight) 0.18+0.82*math.max(0.0,dot+0.12)/(1.0+d*0.025)
else 0.16
shade(t.color,light)
}
def renderWorld(w: Int,h: Int): Unit = {
if (image==null || image.getWidth!=w || image.getHeight!=h) {
image=new BufferedImage(w,h,BufferedImage.TYPE_INT_RGB)
pixels=image.getRaster.getDataBuffer.asInstanceOf[DataBufferInt].getData
zBuffer=new Array[Double](w*h)
}
val bg=image.createGraphics()
bg.setPaint(new GradientPaint(0f,0f,new Color(9,11,15),0f,(h*0.5).toFloat,new Color(47,40,34)))
bg.fillRect(0,0,w,h)
bg.setPaint(new GradientPaint(0f,(h*0.5).toFloat,new Color(47,39,31),0f,h.toFloat,new Color(8,8,10)))
bg.fillRect(0,h/2,w,h/2)
bg.dispose()
java.util.Arrays.fill(zBuffer,0.0)
val dyn=dynamicGeometry()
val all=triangles ++ dyn
all.foreach { t =>
val clipped=clipNear(Vector(camera(t.a),camera(t.b),camera(t.c)))
if (clipped.length>=3) {
val col=lightColor(t)
var i=1
while (i<clipped.length-1) {
rasterTriangle(clipped(0),clipped(i),clipped(i+1),col,w,h)
i+=1
}
}
}
}
def drawMap(g: Graphics2D,w: Int): Unit = {
val ox=w-176; val oy=100; val scale=6.0
g.setColor(new Color(0,0,0,180)); g.fillRect(ox-6,oy-6,150,126)
g.setColor(new Color(125,112,96))
solids.foreach { b =>
if (b.y<2.0) g.fillRect(ox+(b.x-b.sx/2).toInt*scale.toInt,
oy+(b.z-b.sz/2).toInt*scale.toInt,math.max(1,(b.sx*scale).toInt),math.max(1,(b.sz*scale).toInt))
}
g.setColor(Color.RED); g.fillOval(ox+(enemyX*scale).toInt-3,oy+(enemyZ*scale).toInt-3,7,7)
g.setColor(Color.CYAN); g.fillOval(ox+(px*scale).toInt-3,oy+(pz*scale).toInt-3,7,7)
}
def drawHUD(g: Graphics2D,w: Int,h: Int): Unit = {
g.setColor(new Color(0,0,0,175)); g.fillRect(0,0,w,58)
g.setFont(new Font("Arial",Font.BOLD,16)); g.setColor(new Color(245,222,184))
g.drawString("DAY "+day+" KEY: "+(if(hasKey) "YES" else "NO")+
" FLOOR: "+(if(onUpperFloor) "UPSTAIRS" else "GROUND")+
" SECRET: "+(if(secretUnlocked) "OPEN" else "HIDDEN"),14,23)
g.setFont(new Font("Arial",Font.PLAIN,13)); g.setColor(Color.WHITE)
g.drawString("WASD Move | Mouse drag Look | SHIFT Run | E Interact | Q Stun bomb | F Light | M Map | R Restart",14,45)
g.setColor(new Color(245,222,184))
g.drawString("STUN BOMB: "+(if(hasBomb) "READY" else "NONE")+
(if(enemyStunnedTimer>0.0) " PURSUER STUNNED" else "")+
(if(hidden) " HIDING: "+hidePlace else ""),14,76)
g.setColor(new Color(0,0,0,180)); g.fillRect(0,h-39,w,39)
if (messageTimer>0.0) {
g.setFont(new Font("Arial",Font.BOLD,16)); g.setColor(new Color(255,225,170))
g.drawString(message,14,h-14)
}
if (!hidden && enemyUpper==onUpperFloor && dist(px,pz,enemyX,enemyZ)<5.0) {
g.setFont(new Font("Arial",Font.BOLD,15)); g.setColor(new Color(255,70,60))
g.drawString("SOMETHING IS NEAR...",w-235,82)
}
if (gameOver || escaped) {
g.setColor(new Color(0,0,0,220)); g.fillRect(0,0,w,h)
g.setFont(new Font("Arial",Font.BOLD,42)); g.setColor(if(escaped) new Color(90,255,125) else new Color(255,75,60))
val title=if(escaped) "YOU ESCAPED!" else "GAME OVER"
g.drawString(title,(w-g.getFontMetrics.stringWidth(title))/2,h/2)
g.setFont(new Font("Arial",Font.BOLD,20)); g.setColor(Color.WHITE)
g.drawString("Press R to restart",w/2-95,h/2+46)
}
}
def drawIntro(g: Graphics2D,w: Int,h: Int): Unit = {
g.setPaint(new GradientPaint(0f,0f,new Color(5,6,9),w.toFloat,h.toFloat,new Color(59,23,20)))
g.fillRect(0,0,w,h)
g.setColor(new Color(25,20,18)); g.fillRect(80,85,w-160,h-155)
g.setColor(new Color(65,45,33)); g.fillRect(130,120,w-260,h-225)
g.setColor(new Color(32,24,22)); g.fillRect(160,h-170,260,55)
g.setColor(new Color(100,80,65)); g.fillRect(180,h-195,220,32)
g.setColor(new Color(40,24,18)); g.fillRect(w-270,150,90,250)
g.setColor(new Color(210,165,68)); g.fillOval(w-211,275,11,11)
g.setFont(new Font("Arial",Font.BOLD,45)); g.setColor(new Color(222,211,194))
val title="OLD HOUSE 3D"; g.drawString(title,(w-g.getFontMetrics.stringWidth(title))/2,70)
g.setFont(new Font("Arial",Font.BOLD,21)); g.setColor(new Color(255,95,75))
val d="DAY 1"; g.drawString(d,(w-g.getFontMetrics.stringWidth(d))/2,h-105)
g.setFont(new Font("Arial",Font.BOLD,20)); g.setColor(new Color(250,220,160))
val p="PRESS ENTER TO WAKE UP"; g.drawString(p,(w-g.getFontMetrics.stringWidth(p))/2,h-62)
}
class GamePanel extends JPanel {
setPreferredSize(new Dimension(SW,SH)); setFocusable(true); setBackground(Color.BLACK)
setFocusTraversalKeysEnabled(false)
override def paintComponent(graphics: Graphics): Unit = {
super.paintComponent(graphics)
val g=graphics.create().asInstanceOf[Graphics2D]
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON)
if (!started) drawIntro(g,getWidth,getHeight)
else {
renderWorld(getWidth,getHeight)
g.drawImage(image,0,0,null)
g.setColor(new Color(230,230,215,170))
g.drawLine(getWidth/2-5,getHeight/2,getWidth/2+5,getHeight/2)
g.drawLine(getWidth/2,getHeight/2-5,getWidth/2,getHeight/2+5)
drawHUD(g,getWidth,getHeight)
if (showMap) drawMap(g,getWidth)
}
g.dispose()
}
}
def start(): Unit = {
buildHouse(); resetGame()
SwingUtilities.invokeLater(new Runnable {
def run(): Unit = {
frame=new JFrame("OLD HOUSE 3D - Scala Horror Prototype")
panel=new GamePanel()
panel.addKeyListener(new KeyAdapter {
override def keyPressed(e: KeyEvent): Unit = {
val k=e.getKeyCode; keys.add(k)
if (k==KeyEvent.VK_ENTER && !started && !gameOver) wake()
if (k==KeyEvent.VK_E) interact()
if (k==KeyEvent.VK_Q) throwStunBomb()
if (k==KeyEvent.VK_F) { flashlight = !flashlight; say(if(flashlight) "FLASHLIGHT ON" else "FLASHLIGHT OFF") }
if (k==KeyEvent.VK_M) showMap = !showMap
if (k==KeyEvent.VK_R) resetGame()
if (k==KeyEvent.VK_LEFT) yaw -= 0.08
if (k==KeyEvent.VK_RIGHT) yaw += 0.08
}
override def keyReleased(e: KeyEvent): Unit = keys.remove(e.getKeyCode)
})
panel.addMouseListener(new MouseAdapter {
override def mousePressed(e: MouseEvent): Unit = { lastMouseX=e.getX; lastMouseY=e.getY; panel.requestFocusInWindow() }
override def mouseReleased(e: MouseEvent): Unit = { lastMouseX = -1; lastMouseY = -1 }
})
panel.addMouseMotionListener(new MouseMotionAdapter {
override def mouseDragged(e: MouseEvent): Unit = {
if (started && !gameOver && !escaped && lastMouseX>=0) {
val dx=e.getX-lastMouseX; val dy=e.getY-lastMouseY
yaw += dx*0.007; pitch=clamp(pitch-dy*0.004,-0.55,0.55)
lastMouseX=e.getX; lastMouseY=e.getY
}
}
})
panel.addFocusListener(new java.awt.event.FocusAdapter {
override def focusLost(e: java.awt.event.FocusEvent): Unit = keys.clear()
})
frame.setContentPane(panel); frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
frame.addWindowListener(new java.awt.event.WindowAdapter { override def windowClosed(e: java.awt.event.WindowEvent): Unit = { if (gameTimer != null) gameTimer.stop(); keys.clear() } })
frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); panel.requestFocusInWindow()
gameTimer=new Timer(33,new ActionListener { def actionPerformed(e: ActionEvent): Unit = { updateGame(); panel.repaint() } })
gameTimer.start()
}
})
}
}
import java.awt._
import java.awt.event._
import java.awt.image.BufferedImage
import java.awt.image.DataBufferInt
import java.io.File
import javax.imageio.ImageIO
import javax.swing._
import scala.collection.mutable.ArrayBuffer
// ================================================================
// BABASAHEB 3D STATUE STUDIO
// A self-contained Scala + Swing procedural 3D statue viewer.
// No external libraries or copyrighted model files are required.
// It creates a stylized, colored 3D representation inspired by
// the widely used blue-suit, glasses, pointing-hand, Constitution
// book pose of Dr. B. R. Ambedkar.
// ================================================================
object Babasaheb3DStatue {
case class V(x: Double, y: Double, z: Double) {
def +(b: V): V = V(x + b.x, y + b.y, z + b.z)
def -(b: V): V = V(x - b.x, y - b.y, z - b.z)
def *(k: Double): V = V(x * k, y * k, z * k)
}
case class Tri(a: V, b: V, c: V, color: Color)
case class TV(x: Double, y: Double, z: Double)
case class SV(x: Double, y: Double, invZ: Double)
val triangles = ArrayBuffer[Tri]()
// Choose a figure from the dropdown. These are original, procedural,
// low-poly interpretations made only from geometry in this Scala file.
val figureNames = Array(
"Dr. Babasaheb Ambedkar",
"Gautam Buddha",
"Ganesh Ji",
"Lord Shiva",
"Shri Krishna",
"Lord Rama",
"Hanuman Ji",
"Maa Durga",
"Maa Saraswati",
"Maa Lakshmi",
"Lord Vishnu",
"Mahavir Swami",
"Sai Baba",
"Jesus Christ",
"Guru Nanak Dev Ji"
)
var selectedFigure = figureNames(0)
// Camera / viewer state.
var yaw = 0.0
var pitch = 0.02
var zoom = 9.4
val targetY = 2.65
var themeMode = 0
var showHalo = true
var showBook = true
var showStars = true
val backgroundTop = new Color(8, 14, 35)
val backgroundBottom = new Color(45, 27, 57)
val gold = new Color(255, 200, 72)
val classicBlue = new Color(25, 78, 190)
val skinClassic = new Color(190, 137, 105)
val darkClassic = new Color(22, 22, 27)
def clamp(v: Double, lo: Double, hi: Double): Double =
math.max(lo, math.min(hi, v))
def dot(a: V, b: V): Double = a.x * b.x + a.y * b.y + a.z * b.z
def cross(a: V, b: V): V = V(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
)
def length(v: V): Double = math.sqrt(dot(v, v))
def norm(v: V): V = {
val n = length(v)
if (n < 0.000001) V(0, 1, 0) else V(v.x / n, v.y / n, v.z / n)
}
def recolor(role: String, c: Color): Color = {
if (themeMode == 0) c
else if (themeMode == 1) { // bronze sculpture
role match {
case "glasses" | "eye" | "dark" => new Color(45, 31, 25)
case "gold" => new Color(222, 167, 69)
case "base" => new Color(59, 47, 42)
case "background" => c
case _ => new Color(151, 91, 53)
}
} else if (themeMode == 2) { // golden tribute statue
role match {
case "glasses" | "eye" | "dark" => new Color(58, 39, 19)
case "base" => new Color(22, 39, 83)
case "background" => c
case _ => new Color(236, 177, 43)
}
} else { // marble
role match {
case "glasses" | "eye" | "dark" => new Color(51, 55, 66)
case "gold" => new Color(170, 139, 76)
case "base" => new Color(62, 72, 91)
case "background" => c
case _ => new Color(220, 222, 224)
}
}
}
def scaleColor(c: Color, factor: Double): Color = new Color(
clamp(c.getRed * factor, 0, 255).toInt,
clamp(c.getGreen * factor, 0, 255).toInt,
clamp(c.getBlue * factor, 0, 255).toInt
)
def addTri(a: V, b: V, c: V, base: Color): Unit = {
val n = norm(cross(b - a, c - a))
val light = norm(V(-0.38, 0.82, 0.58))
val diffuse = math.abs(dot(n, light))
val factor = 0.47 + 0.53 * diffuse
triangles += Tri(a, b, c, scaleColor(base, factor))
}
def addBox(cx: Double, cy: Double, cz: Double,
sx: Double, sy: Double, sz: Double, color: Color): Unit = {
val x0 = cx - sx / 2; val x1 = cx + sx / 2
val y0 = cy - sy / 2; val y1 = cy + sy / 2
val z0 = cz - sz / 2; val z1 = cz + sz / 2
val v = Array(
V(x0,y0,z0), V(x1,y0,z0), V(x1,y1,z0), V(x0,y1,z0),
V(x0,y0,z1), V(x1,y0,z1), V(x1,y1,z1), V(x0,y1,z1)
)
val f = Array(
Array(0,1,2), Array(0,2,3), Array(4,7,6), Array(4,6,5),
Array(0,4,5), Array(0,5,1), Array(3,2,6), Array(3,6,7),
Array(0,3,7), Array(0,7,4), Array(1,5,6), Array(1,6,2)
)
f.foreach(k => addTri(v(k(0)), v(k(1)), v(k(2)), color))
}
def addEllipsoid(center: V, rx: Double, ry: Double, rz: Double,
color: Color, lat: Int = 14, lon: Int = 22): Unit = {
def point(i: Int, j: Int): V = {
val phi = math.Pi * i.toDouble / lat
val theta = 2.0 * math.Pi * j.toDouble / lon
V(center.x + rx * math.sin(phi) * math.cos(theta),
center.y + ry * math.cos(phi),
center.z + rz * math.sin(phi) * math.sin(theta))
}
for (i <- 0 until lat; j <- 0 until lon) {
val a = point(i, j)
val b = point(i + 1, j)
val c = point(i + 1, (j + 1) % lon)
val d = point(i, (j + 1) % lon)
addTri(a, b, c, color)
addTri(a, c, d, color)
}
}
// Cylinder / capsule-like limb between two 3D points.
def addCylinder(a: V, b: V, rA: Double, rB: Double,
color: Color, seg: Int = 12, caps: Boolean = true): Unit = {
val axis = norm(b - a)
val helper = if (math.abs(axis.y) < 0.88) V(0,1,0) else V(1,0,0)
val u = norm(cross(axis, helper))
val v = norm(cross(axis, u))
val ringA = (0 until seg).map { i =>
val t = 2.0 * math.Pi * i / seg
a + u * (math.cos(t) * rA) + v * (math.sin(t) * rA)
}.toVector
val ringB = (0 until seg).map { i =>
val t = 2.0 * math.Pi * i / seg
b + u * (math.cos(t) * rB) + v * (math.sin(t) * rB)
}.toVector
for (i <- 0 until seg) {
val j = (i + 1) % seg
addTri(ringA(i), ringA(j), ringB(j), color)
addTri(ringA(i), ringB(j), ringB(i), color)
if (caps) {
addTri(a, ringA(j), ringA(i), color)
addTri(b, ringB(i), ringB(j), color)
}
}
}
def addCircle(cx: Double, cy: Double, z: Double, radius: Double,
tube: Double, color: Color, seg: Int = 40): Unit = {
for (i <- 0 until seg) {
val t0 = 2.0 * math.Pi * i / seg
val t1 = 2.0 * math.Pi * (i + 1) / seg
val a = V(cx + radius * math.cos(t0), cy + radius * math.sin(t0), z)
val b = V(cx + radius * math.cos(t1), cy + radius * math.sin(t1), z)
addCylinder(a, b, tube, tube, color, 6, false)
}
}
def addTorusXY(cx: Double, cy: Double, cz: Double, major: Double,
tube: Double, color: Color, sides: Int = 72,
rings: Int = 8): Unit = {
def p(i: Int, j: Int): V = {
val t = 2.0 * math.Pi * i / sides
val u = 2.0 * math.Pi * j / rings
V(cx + (major + tube * math.cos(u)) * math.cos(t),
cy + (major + tube * math.cos(u)) * math.sin(t),
cz + tube * math.sin(u))
}
for (i <- 0 until sides; j <- 0 until rings) {
val a = p(i,j); val b = p((i+1)%sides,j)
val c = p((i+1)%sides,(j+1)%rings); val d = p(i,(j+1)%rings)
addTri(a,b,c,color); addTri(a,c,d,color)
}
}
def addGround(): Unit = {
val c = new Color(28, 34, 54)
val a = V(-24, -0.035, -22); val b = V(24, -0.035, -22)
val d = V(-24, -0.035, 22); val e = V(24, -0.035, 22)
addTri(a,d,e,c); addTri(a,e,b,c)
}
def C(role: String, color: Color): Color = recolor(role, color)
// --------------------------------------------------------------
// BUILD THE STATUE: face, glasses, pointing arm, book and suit.
// --------------------------------------------------------------
def buildScene(): Unit = {
triangles.clear()
addGround()
// Decorative halo stays behind the statue.
if (showHalo) {
addTorusXY(0, 2.82, -0.88, 2.23, 0.065, C("gold", gold), 72, 8)
addTorusXY(0, 2.82, -0.90, 2.05, 0.018, C("gold", new Color(220, 169, 60)), 72, 6)
for (i <- 0 until 28) {
val a = i * 2.0 * math.Pi / 28.0
val p1 = V(2.38 * math.cos(a), 2.82 + 2.38 * math.sin(a), -0.86)
val p2 = V(2.58 * math.cos(a), 2.82 + 2.58 * math.sin(a), -0.86)
addCylinder(p1, p2, 0.018, 0.012, C("gold", gold), 6, false)
}
}
// Gentle distant decorative lights.
if (showStars) {
val stars = Array(
(-3.0,4.45),(-2.8,2.1),(-2.45,3.7),(2.8,4.3),(3.0,2.8),
(2.6,1.25),(-2.5,1.25),(1.9,4.75),(-1.8,4.8),(3.25,3.65)
)
stars.foreach { s =>
addEllipsoid(V(s._1,s._2,-1.15),0.035,0.035,0.025,
C("gold",new Color(255, 219, 127)),8,10)
}
}
// Pedestal: layered circular 3D plinth.
addCylinder(V(0,0.02,0), V(0,0.24,0), 1.02, 1.02,
C("base",new Color(20,46,103)), 48, true)
addCylinder(V(0,0.24,0), V(0,0.53,0), 0.84, 0.84,
C("base",new Color(25,65,139)), 48, true)
addCylinder(V(0,0.53,0), V(0,0.80,0), 0.73, 0.68,
C("base",new Color(24,48,96)), 48, true)
addCylinder(V(0,0.79,0), V(0,0.84,0), 0.70, 0.70,
C("gold",new Color(218,174,72)), 48, true)
addBox(0,0.41,0.70,1.48,0.36,0.12,C("base",new Color(15,36,83)))
addBox(0,0.41,0.768,1.34,0.25,0.025,C("gold",new Color(197,151,58)))
addBox(0,0.41,0.786,1.28,0.20,0.018,C("base",new Color(19,41,89)))
if (selectedFigure == "Dr. Babasaheb Ambedkar") {
// Shoes, trousers and polished black shoes.
addEllipsoid(V(-0.23,0.91,0.10),0.22,0.12,0.34,C("dark",new Color(20,22,27)),12,20)
addEllipsoid(V(0.23,0.91,0.10),0.22,0.12,0.34,C("dark",new Color(20,22,27)),12,20)
addBox(-0.23,0.99,0.07,0.26,0.08,0.35,C("dark",new Color(38,39,43)))
addBox(0.23,0.99,0.07,0.26,0.08,0.35,C("dark",new Color(38,39,43)))
val pants = C("suit",new Color(24,70,163))
addCylinder(V(-0.23,0.98,0.00),V(-0.29,2.04,-0.01),0.19,0.23,pants,18,true)
addCylinder(V(0.23,0.98,0.00),V(0.29,2.04,-0.01),0.19,0.23,pants,18,true)
addEllipsoid(V(0,2.00,-0.01),0.43,0.30,0.29,pants,14,22)
addBox(-0.24,1.55,0.00,0.28,0.78,0.34,pants)
addBox(0.24,1.55,0.00,0.28,0.78,0.34,pants)
// Shirt and main suit jacket.
val suit = C("suit",classicBlue)
val shirt = C("shirt",new Color(241,238,226))
val skin = C("skin",skinClassic)
val tie = C("tie",new Color(188,27,47))
val hair = C("dark",new Color(25,24,27))
val jacketDark = scaleColor(suit,0.78)
addEllipsoid(V(0,2.72,-0.015),0.65,0.88,0.36,suit,20,28)
addEllipsoid(V(-0.48,3.36,-0.015),0.29,0.29,0.31,suit,14,20)
addEllipsoid(V(0.48,3.36,-0.015),0.29,0.29,0.31,suit,14,20)
addEllipsoid(V(0,3.46,-0.025),0.38,0.24,0.30,suit,14,20)
// White shirt panel on the front of the suit.
addEllipsoid(V(0,2.91,0.285),0.255,0.66,0.105,shirt,18,24)
addTri(V(-0.24,3.41,0.365),V(-0.04,3.12,0.405),V(-0.13,3.47,0.36),
C("shirt",new Color(247,245,236)))
addTri(V(0.24,3.41,0.365),V(0.04,3.12,0.405),V(0.13,3.47,0.36),
C("shirt",new Color(247,245,236)))
// Lapels, jacket seams and buttons.
addTri(V(-0.42,3.43,0.27),V(-0.12,3.02,0.40),V(-0.27,2.72,0.38),jacketDark)
addTri(V(0.42,3.43,0.27),V(0.12,3.02,0.40),V(0.27,2.72,0.38),jacketDark)
addTri(V(-0.10,3.42,0.39),V(-0.01,3.10,0.415),V(0.04,3.41,0.39),
C("shirt",new Color(245,241,230)))
addTri(V(-0.015,3.10,0.416),V(0.04,3.41,0.39),V(0.13,3.10,0.405),
C("shirt",new Color(245,241,230)))
addTri(V(-0.09,3.22,0.43),V(0,3.11,0.445),V(0.09,3.22,0.43),tie)
addTri(V(-0.085,3.17,0.44),V(0.085,3.17,0.44),V(0.025,2.43,0.44),tie)
addTri(V(-0.085,3.17,0.44),V(0.025,2.43,0.44),V(-0.025,2.43,0.44),tie)
for (yy <- Seq(2.55,2.78,3.00)) {
addEllipsoid(V(0.28,yy,0.345),0.028,0.028,0.022,C("gold",new Color(205,167,72)),8,10)
}
addBox(0.39,2.96,0.34,0.20,0.24,0.035,jacketDark)
addBox(0.39,2.96,0.364,0.15,0.18,0.018,suit)
// Right arm: extended to point outward (on the viewer's left).
addCylinder(V(-0.53,3.37,0.00),V(-0.97,3.68,0.08),0.20,0.175,suit,18,true)
addEllipsoid(V(-0.77,3.55,0.05),0.21,0.22,0.20,suit,14,18)
addCylinder(V(-0.97,3.68,0.08),V(-1.47,4.01,0.13),0.16,0.125,suit,18,true)
addCylinder(V(-1.45,4.00,0.13),V(-1.61,4.055,0.15),0.13,0.105,
C("shirt",new Color(238,234,222)),12,true)
addEllipsoid(V(-1.72,4.085,0.18),0.19,0.12,0.13,skin,14,18)
// Pointing index finger and curled remaining fingers.
addCylinder(V(-1.78,4.13,0.21),V(-2.22,4.32,0.22),0.047,0.027,skin,10,true)
addEllipsoid(V(-2.235,4.326,0.22),0.038,0.032,0.032,skin,8,10)
addCylinder(V(-1.82,4.07,0.22),V(-2.04,4.04,0.24),0.039,0.029,skin,10,true)
addCylinder(V(-1.80,4.04,0.22),V(-1.98,3.985,0.23),0.035,0.026,skin,10,true)
addCylinder(V(-1.75,4.02,0.22),V(-1.90,3.965,0.22),0.032,0.024,skin,10,true)
addCylinder(V(-1.67,4.04,0.22),V(-1.76,3.98,0.25),0.034,0.027,skin,10,true)
// Left arm: bent in toward the Constitution book.
addCylinder(V(0.51,3.34,0.00),V(0.78,2.96,0.22),0.19,0.16,suit,18,true)
addEllipsoid(V(0.68,3.13,0.12),0.19,0.22,0.19,suit,14,18)
addCylinder(V(0.78,2.96,0.22),V(0.59,2.64,0.42),0.15,0.12,suit,18,true)
addCylinder(V(0.59,2.66,0.42),V(0.54,2.58,0.51),0.12,0.09,
C("shirt",new Color(240,237,225)),12,true)
// Constitution book with cover, pages, gold border and page lines.
if (showBook) {
addBox(0.43,2.72,0.53,0.48,0.67,0.18,C("book",new Color(100,51,28)))
addBox(0.43,2.72,0.635,0.425,0.61,0.025,C("book",new Color(115,61,31)))
addBox(0.43,2.72,0.653,0.38,0.56,0.012,C("gold",new Color(212,174,83)))
addBox(0.43,2.72,0.661,0.35,0.53,0.01,C("book",new Color(99,47,25)))
for (yy <- Seq(2.57,2.62,2.67,2.72,2.77,2.82,2.87)) {
addBox(0.43,yy,0.67,0.25,0.008,0.006,C("gold",new Color(225,194,117)))
}
addBox(0.43,3.015,0.663,0.34,0.012,0.008,C("gold",new Color(231,202,132)))
}
// Neck, collar, head and ears.
addCylinder(V(0,3.38,0.00),V(0,3.78,0.00),0.19,0.17,skin,18,true)
addEllipsoid(V(0,3.70,0.06),0.27,0.14,0.23,shirt,14,20)
addEllipsoid(V(0,4.22,0.025),0.365,0.49,0.315,skin,24,32)
addEllipsoid(V(-0.355,4.19,0.015),0.075,0.12,0.07,skin,12,16)
addEllipsoid(V(0.355,4.19,0.015),0.075,0.12,0.07,skin,12,16)
// Neatly combed dark hair cap / side sections.
addEllipsoid(V(0,4.585,-0.005),0.355,0.145,0.30,hair,16,26)
addEllipsoid(V(-0.30,4.43,0.005),0.09,0.19,0.26,hair,12,18)
addEllipsoid(V(0.30,4.43,0.005),0.09,0.19,0.26,hair,12,18)
addEllipsoid(V(0,4.50,0.275),0.26,0.045,0.055,hair,10,18)
// Eyes, pupils, brows, nose, mouth and iconic round glasses.
addEllipsoid(V(-0.135,4.265,0.308),0.061,0.043,0.027,
C("shirt",new Color(247,238,221)),10,14)
addEllipsoid(V(0.135,4.265,0.308),0.061,0.043,0.027,
C("shirt",new Color(247,238,221)),10,14)
addEllipsoid(V(-0.132,4.265,0.337),0.022,0.027,0.014,C("eye",new Color(28,25,24)),8,10)
addEllipsoid(V(0.138,4.265,0.337),0.022,0.027,0.014,C("eye",new Color(28,25,24)),8,10)
addCylinder(V(-0.20,4.333,0.32),V(-0.07,4.342,0.326),0.018,0.016,hair,8,true)
addCylinder(V(0.07,4.342,0.326),V(0.20,4.333,0.32),0.016,0.018,hair,8,true)
val glasses = C("glasses",new Color(28,29,34))
addCircle(-0.135,4.265,0.36,0.105,0.014,glasses,32)
addCircle(0.135,4.265,0.36,0.105,0.014,glasses,32)
addCylinder(V(-0.03,4.27,0.36),V(0.03,4.27,0.36),0.014,0.014,glasses,8,false)
addCylinder(V(-0.24,4.28,0.34),V(-0.34,4.24,0.17),0.012,0.009,glasses,8,false)
addCylinder(V(0.24,4.28,0.34),V(0.34,4.24,0.17),0.012,0.009,glasses,8,false)
addEllipsoid(V(0,4.13,0.345),0.065,0.105,0.075,skin,12,16)
addEllipsoid(V(0,4.075,0.395),0.038,0.028,0.027,
C("skin",new Color(181,121,91)),10,12)
addCylinder(V(-0.085,3.995,0.316),V(0.085,3.995,0.316),0.013,0.013,
C("dark",new Color(102,54,47)),8,false)
addEllipsoid(V(0,3.945,0.302),0.085,0.035,0.035,
C("skin",new Color(177,117,91)),10,14)
// Small decorative lapel pin.
addEllipsoid(V(-0.34,3.18,0.367),0.045,0.045,0.02,
C("gold",new Color(248,203,76)),10,14)
} else {
buildOtherFigure()
}
}
// --------------------------------------------------------------
// OTHER ORIGINAL PROCEDURAL STATUES
// These are stylized 3D interpretations, not photo-realistic assets.
// --------------------------------------------------------------
def addFace(skin0: Color, hair0: Color, cy: Double,
hasHair: Boolean = true, closedEyes: Boolean = false,
foreheadMark: Option[Color] = None): Unit = {
val skin = C("skin", skin0)
val hair = C("dark", hair0)
addCylinder(V(0,cy-0.48,0.00), V(0,cy-0.28,0.00), 0.15,0.14,skin,14,true)
addEllipsoid(V(0,cy,0.05),0.325,0.425,0.285,skin,20,28)
addEllipsoid(V(-0.323,cy-0.01,0.02),0.065,0.12,0.07,skin,10,14)
addEllipsoid(V(0.323,cy-0.01,0.02),0.065,0.12,0.07,skin,10,14)
if (hasHair) {
addEllipsoid(V(0,cy+0.30,-0.015),0.325,0.15,0.275,hair,14,24)
addEllipsoid(V(-0.275,cy+0.19,0.0),0.075,0.18,0.24,hair,10,14)
addEllipsoid(V(0.275,cy+0.19,0.0),0.075,0.18,0.24,hair,10,14)
}
if (closedEyes) {
addCylinder(V(-0.19,cy+0.015,0.307),V(-0.075,cy+0.005,0.322),0.012,0.012,C("dark",new Color(65,40,32)),8,false)
addCylinder(V(0.075,cy+0.005,0.322),V(0.19,cy+0.015,0.307),0.012,0.012,C("dark",new Color(65,40,32)),8,false)
} else {
addEllipsoid(V(-0.125,cy+0.015,0.315),0.047,0.029,0.018,new Color(247,239,222),8,12)
addEllipsoid(V(0.125,cy+0.015,0.315),0.047,0.029,0.018,new Color(247,239,222),8,12)
addEllipsoid(V(-0.125,cy+0.015,0.334),0.018,0.022,0.012,new Color(32,25,22),8,10)
addEllipsoid(V(0.125,cy+0.015,0.334),0.018,0.022,0.012,new Color(32,25,22),8,10)
addCylinder(V(-0.19,cy+0.075,0.303),V(-0.07,cy+0.083,0.31),0.013,0.013,hair,8,false)
addCylinder(V(0.07,cy+0.083,0.31),V(0.19,cy+0.075,0.303),0.013,0.013,hair,8,false)
}
addEllipsoid(V(0,cy-0.085,0.335),0.042,0.075,0.04,skin,10,12)
addCylinder(V(-0.075,cy-0.19,0.305),V(0.075,cy-0.19,0.305),0.012,0.012,C("dark",new Color(110,62,52)),8,false)
foreheadMark.foreach { mark =>
addEllipsoid(V(0,cy+0.17,0.333),0.035,0.07,0.02,mark,10,12)
}
}
def addCrown(col0: Color, baseY: Double = 4.34): Unit = {
val col = C("gold", col0)
addCylinder(V(0,baseY,0.01),V(0,baseY+0.17,0.01),0.31,0.27,col,20,true)
addCylinder(V(0,baseY+0.17,0.01),V(0,baseY+0.40,0.01),0.27,0.12,col,18,true)
addEllipsoid(V(0,baseY+0.42,0.01),0.10,0.08,0.10,col,10,14)
addEllipsoid(V(0,baseY+0.10,0.285),0.06,0.07,0.035,new Color(45,170,190),8,10)
}
def addStandingLegs(legColor0: Color): Unit = {
val leg = C("robe", legColor0)
addEllipsoid(V(-0.22,1.15,0.07),0.20,0.25,0.31,leg,12,18)
addEllipsoid(V(0.22,1.15,0.07),0.20,0.25,0.31,leg,12,18)
addEllipsoid(V(-0.23,0.92,0.18),0.22,0.10,0.32,C("dark",new Color(62,43,31)),12,18)
addEllipsoid(V(0.23,0.92,0.18),0.22,0.10,0.32,C("dark",new Color(62,43,31)),12,18)
}
def addStandingTorso(skin0: Color, cloth0: Color, sash0: Color): Unit = {
val skin = C("skin",skin0)
val cloth = C("robe",cloth0)
val sash = C("gold",sash0)
addStandingLegs(cloth)
addEllipsoid(V(0,1.72,0.00),0.48,0.40,0.34,cloth,16,22)
addEllipsoid(V(0,2.58,0.00),0.52,0.76,0.34,cloth,18,24)
addCylinder(V(0,3.13,0.00),V(0,3.48,0.00),0.15,0.14,skin,14,true)
addCylinder(V(-0.42,2.88,0.00),V(-0.70,2.47,0.16),0.15,0.12,cloth,14,true)
addCylinder(V(-0.70,2.47,0.16),V(-0.72,2.21,0.34),0.12,0.095,skin,12,true)
addEllipsoid(V(-0.72,2.18,0.36),0.13,0.12,0.12,skin,10,14)
addCylinder(V(0.42,2.88,0.00),V(0.70,2.47,0.16),0.15,0.12,cloth,14,true)
addCylinder(V(0.70,2.47,0.16),V(0.72,2.21,0.34),0.12,0.095,skin,12,true)
addEllipsoid(V(0.72,2.18,0.36),0.13,0.12,0.12,skin,10,14)
addTri(V(-0.27,3.0,0.30),V(0.26,2.28,0.37),V(0.05,2.05,0.38),sash)
addCylinder(V(-0.39,1.98,0.27),V(0.39,1.98,0.27),0.035,0.035,sash,8,false)
}
def buildBuddha(): Unit = {
val skin = new Color(198,145,109)
val robe = C("robe",new Color(208,116,35))
val fold = C("gold",new Color(245,183,68))
// Seated lotus posture and draped robe.
addEllipsoid(V(0,1.31,0.06),0.76,0.30,0.47,robe,18,26)
addEllipsoid(V(-0.39,1.26,0.18),0.43,0.22,0.40,robe,14,22)
addEllipsoid(V(0.39,1.26,0.18),0.43,0.22,0.40,robe,14,22)
addEllipsoid(V(0,1.82,0.01),0.54,0.46,0.39,robe,18,24)
addEllipsoid(V(0,2.39,0.00),0.52,0.62,0.35,robe,18,24)
addEllipsoid(V(-0.43,2.57,0.00),0.25,0.27,0.27,robe,12,18)
addEllipsoid(V(0.43,2.57,0.00),0.25,0.27,0.27,robe,12,18)
addCylinder(V(-0.43,2.55,0.02),V(-0.28,2.02,0.35),0.16,0.12,robe,14,true)
addCylinder(V(0.43,2.55,0.02),V(0.28,2.02,0.35),0.16,0.12,robe,14,true)
addEllipsoid(V(-0.16,1.96,0.40),0.24,0.10,0.12,skin,12,18)
addEllipsoid(V(0.16,1.98,0.42),0.24,0.10,0.12,skin,12,18)
addTri(V(-0.36,2.79,0.28),V(0.14,2.15,0.40),V(0.30,2.78,0.27),fold)
addCylinder(V(0,2.72,0.00),V(0,3.05,0.00),0.14,0.14,skin,14,true)
addFace(skin,new Color(65,45,31),3.45,false,true,None)
addEllipsoid(V(0,3.86,0.04),0.19,0.15,0.18,skin,12,18) // ushnisha
// Small curled hair dots, intentionally understated.
for (row <- 0 until 3; col <- -4 to 4 if math.abs(col)+row < 7) {
addEllipsoid(V(col*0.064,3.67+row*0.062,0.245-row*0.018),0.026,0.026,0.022,
new Color(48,44,39),6,8)
}
addTorusXY(0,1.72,0.25,0.42,0.025,fold,36,6)
}
def buildGanesh(): Unit = {
val skin = new Color(218,151,133)
val robe = C("robe",new Color(191,85,40))
val goldC = C("gold",new Color(245,190,55))
// Seated body and round belly.
addEllipsoid(V(0,1.25,0.08),0.65,0.28,0.43,C("robe",new Color(226,157,54)),16,22)
addEllipsoid(V(0,1.90,0.00),0.50,0.44,0.37,robe,16,22)
addEllipsoid(V(0,2.34,0.00),0.48,0.58,0.34,robe,16,22)
addCylinder(V(-0.43,2.52,0.00),V(-0.82,2.16,0.25),0.14,0.10,robe,12,true)
addCylinder(V(-0.82,2.16,0.25),V(-0.73,2.03,0.36),0.10,0.08,skin,10,true)
addCylinder(V(0.43,2.52,0.00),V(0.82,2.16,0.25),0.14,0.10,robe,12,true)
addCylinder(V(0.82,2.16,0.25),V(0.73,2.03,0.36),0.10,0.08,skin,10,true)
// Elephant head, broad ears, trunk and tusks.
addCylinder(V(0,2.67,0.00),V(0,3.00,0.00),0.15,0.15,skin,12,true)
addEllipsoid(V(0,3.50,0.03),0.40,0.46,0.34,skin,18,24)
addEllipsoid(V(-0.48,3.56,-0.01),0.30,0.19,0.10,skin,12,18)
addEllipsoid(V(0.48,3.56,-0.01),0.30,0.19,0.10,skin,12,18)
addEllipsoid(V(-0.14,3.59,0.342),0.055,0.035,0.022,new Color(35,29,27),8,10)
addEllipsoid(V(0.14,3.59,0.342),0.055,0.035,0.022,new Color(35,29,27),8,10)
addCylinder(V(-0.20,3.39,0.32),V(-0.16,3.26,0.37),0.045,0.03,new Color(248,231,193),8,true)
addCylinder(V(0.20,3.39,0.32),V(0.16,3.26,0.37),0.045,0.03,new Color(248,231,193),8,true)
addCylinder(V(0.02,3.49,0.36),V(0.05,3.19,0.48),0.13,0.10,skin,12,true)
addCylinder(V(0.05,3.19,0.48),V(0.19,3.06,0.45),0.10,0.075,skin,12,true)
addCrown(goldC,3.88)
// Four symbolic arms.
addCylinder(V(-0.39,2.55,0.00),V(-0.86,2.85,0.05),0.12,0.09,robe,12,true)
addEllipsoid(V(-0.91,2.88,0.08),0.13,0.12,0.11,skin,10,12)
addCylinder(V(0.39,2.55,0.00),V(0.86,2.85,0.05),0.12,0.09,robe,12,true)
addEllipsoid(V(0.91,2.88,0.08),0.13,0.12,0.11,skin,10,12)
addEllipsoid(V(-0.95,2.90,0.08),0.09,0.10,0.08,goldC,10,12)
addEllipsoid(V(0.95,2.90,0.08),0.09,0.10,0.08,goldC,10,12)
// Small mouse companion on the pedestal.
addEllipsoid(V(0.65,0.94,0.40),0.17,0.09,0.13,new Color(105,100,91),10,14)
addEllipsoid(V(0.76,1.00,0.40),0.07,0.07,0.07,new Color(105,100,91),8,10)
}
def buildShiva(): Unit = {
val blueSkin = new Color(85,153,205)
addStandingTorso(blueSkin,new Color(224,182,81),new Color(239,203,103))
addFace(blueSkin,new Color(50,43,37),4.00,true,false,Some(new Color(195,35,40)))
addEllipsoid(V(0,4.45,-0.02),0.22,0.26,0.21,new Color(51,47,40),12,18)
addEllipsoid(V(0.16,4.58,0.00),0.095,0.18,0.08,new Color(60,54,45),10,14)
// Crescent ornament above the matted hair.
addCylinder(V(-0.15,4.60,0.05),V(-0.06,4.76,0.05),0.025,0.025,new Color(228,229,235),8,false)
addCylinder(V(-0.06,4.76,0.05),V(0.08,4.78,0.05),0.025,0.025,new Color(228,229,235),8,false)
// Rudraksha beads and snake coil.
addTorusXY(0,3.43,0.22,0.19,0.035,new Color(92,62,36),40,6)
for (i <- 0 until 18) {
val a = i*2*math.Pi/18.0
addEllipsoid(V(0.22*math.cos(a),3.43+0.22*math.sin(a),0.245),0.025,0.025,0.022,
new Color(126,84,49),6,8)
}
// Trident.
val metal = C("gold",new Color(219,194,129))
addCylinder(V(1.02,0.94,-0.02),V(1.02,4.48,-0.02),0.035,0.028,WOODEN,10,true)
addCylinder(V(1.02,4.08,-0.02),V(0.88,4.45,-0.02),0.045,0.012,metal,8,true)
addCylinder(V(1.02,4.08,-0.02),V(1.16,4.45,-0.02),0.045,0.012,metal,8,true)
addCylinder(V(1.02,4.08,-0.02),V(1.02,4.57,-0.02),0.045,0.015,metal,8,true)
}
// Shared colour for sacred wooden props.
val WOODEN = new Color(105,68,42)
def buildKrishna(): Unit = {
val skin = new Color(76,133,199)
addStandingTorso(skin,new Color(240,190,48),new Color(54,156,96))
addFace(skin,new Color(28,25,23),4.00,true,false,Some(new Color(220,43,48)))
addCrown(new Color(224,174,49),4.37)
// Peacock feather.
addCylinder(V(0.13,4.68,0.02),V(0.29,4.96,0.00),0.025,0.016,new Color(41,114,66),8,true)
addEllipsoid(V(0.31,4.98,0.01),0.10,0.15,0.035,new Color(35,144,73),10,16)
addEllipsoid(V(0.32,4.98,0.045),0.055,0.08,0.02,new Color(40,112,193),8,12)
addEllipsoid(V(0.32,4.98,0.063),0.026,0.045,0.012,new Color(239,191,48),8,10)
// Flute across the chest.
addCylinder(V(-0.67,2.63,0.38),V(0.68,2.68,0.38),0.035,0.035,new Color(214,168,72),12,true)
for (x <- Seq(-0.34,-0.12,0.12,0.34)) {
addEllipsoid(V(x,2.64,0.415),0.018,0.018,0.01,new Color(54,46,33),6,8)
}
}
def buildRama(): Unit = {
val skin = new Color(93,151,212)
addStandingTorso(skin,new Color(230,175,49),new Color(194,48,42))
addFace(skin,new Color(43,34,26),4.00,true,false,Some(new Color(190,35,37)))
addCrown(new Color(224,174,49),4.37)
// Tall curved bow at the figure's left.
val wood = new Color(118,70,37)
val pts = Vector(V(-1.05,1.20,0.04),V(-1.20,1.80,0.04),V(-1.26,2.55,0.04),V(-1.16,3.30,0.04),V(-0.94,4.00,0.04))
pts.sliding(2).foreach(pair => if(pair.length==2) addCylinder(pair(0),pair(1),0.035,0.028,wood,8,true))
addCylinder(pts.head,pts.last,0.008,0.008,new Color(224,217,194),6,false)
addCylinder(V(-0.69,2.78,0.10),V(-0.89,2.98,0.10),0.06,0.035,wood,8,true)
}
def buildHanuman(): Unit = {
val skin = new Color(199,121,67)
val cloth = new Color(207,82,42)
addStandingTorso(skin,cloth,new Color(243,185,52))
addFace(skin,new Color(117,66,36),4.00,true,false,Some(new Color(211,35,35)))
// Broad muzzle and divine crown.
addEllipsoid(V(0,3.88,0.34),0.18,0.12,0.11,new Color(220,159,113),12,16)
addEllipsoid(V(-0.10,3.90,0.43),0.025,0.025,0.018,new Color(36,25,20),8,10)
addEllipsoid(V(0.10,3.90,0.43),0.025,0.025,0.018,new Color(36,25,20),8,10)
addCrown(new Color(227,173,48),4.38)
// Curled tail behind the body.
val tail = Vector(V(0.36,1.55,-0.16),V(0.74,1.90,-0.27),V(0.80,2.32,-0.27),V(0.64,2.55,-0.25))
tail.sliding(2).foreach(pair => if(pair.length==2) addCylinder(pair(0),pair(1),0.05,0.04,skin,8,true))
// Gada (mace).
addCylinder(V(1.03,1.0,-0.02),V(0.93,3.38,-0.02),0.055,0.035,new Color(155,99,42),10,true)
addEllipsoid(V(0.92,3.48,-0.02),0.20,0.24,0.18,new Color(211,161,63),12,16)
}
def buildDurga(): Unit = {
val skin = new Color(215,153,123)
val red = C("robe",new Color(174,28,53))
addStandingLegs(new Color(186,37,56))
addEllipsoid(V(0,1.80,0),0.53,0.43,0.37,new Color(196,36,57),16,22)
addEllipsoid(V(0,2.58,0),0.57,0.78,0.37,red,18,24)
addTri(V(-0.40,3.20,0.31),V(0.36,2.14,0.39),V(0.10,1.90,0.40),new Color(239,186,65))
addCylinder(V(0,3.10,0),V(0,3.47,0),0.15,0.14,skin,14,true)
addFace(skin,new Color(55,35,25),4.00,true,false,Some(new Color(203,27,45)))
addCrown(new Color(236,182,56),4.38)
// Six arms, symbolically holding divine tools.
val armEnds = Seq((-1.05,3.18),(1.05,3.18),(-1.18,2.65),(1.18,2.65),(-0.95,2.14),(0.95,2.14))
armEnds.zipWithIndex.foreach { case ((ex,ey),i) =>
val sy = if (i<2) 3.05 else if (i<4) 2.70 else 2.35
val sx = if (ex<0) -0.43 else 0.43
addCylinder(V(sx,sy,0.0),V(ex,ey,0.12),0.12,0.08,red,10,true)
addEllipsoid(V(ex,ey,0.13),0.10,0.11,0.10,skin,10,12)
}
// A lion companion sits beside the pedestal.
addEllipsoid(V(0.83,1.06,0.34),0.26,0.18,0.25,new Color(197,139,49),12,16)
addEllipsoid(V(1.02,1.18,0.37),0.15,0.14,0.15,new Color(221,167,61),10,14)
addEllipsoid(V(1.08,1.20,0.48),0.035,0.03,0.022,new Color(25,23,22),8,10)
}
def buildSaraswati(): Unit = {
val skin = new Color(218,169,139)
val white = C("robe",new Color(236,229,207))
addStandingTorso(skin,white,new Color(211,173,76))
addFace(skin,new Color(57,40,30),4.00,true,false,None)
addCrown(new Color(220,175,66),4.37)
// Veena diagonally across body, built from simple 3D parts.
addCylinder(V(-0.90,2.15,0.38),V(0.72,3.10,0.35),0.045,0.035,new Color(143,80,37),12,true)
addEllipsoid(V(-0.84,2.12,0.39),0.24,0.21,0.15,new Color(174,103,50),12,16)
addEllipsoid(V(0.69,3.08,0.36),0.12,0.16,0.10,new Color(196,132,63),10,14)
for (i <- 0 until 5) {
addCylinder(V(-0.55,2.35+i*0.12,0.425),V(0.48,2.95+i*0.03,0.40),0.008,0.008,new Color(236,214,153),6,false)
}
// Swan at the side.
addEllipsoid(V(0.88,0.98,0.33),0.20,0.11,0.26,new Color(243,239,220),10,14)
addCylinder(V(0.96,1.02,0.35),V(1.03,1.22,0.35),0.05,0.035,new Color(243,239,220),8,true)
addEllipsoid(V(1.04,1.23,0.35),0.09,0.08,0.08,new Color(243,239,220),8,10)
addCylinder(V(1.09,1.22,0.42),V(1.18,1.20,0.42),0.025,0.01,new Color(223,137,45),6,true)
}
def buildLakshmi(): Unit = {
val skin = new Color(219,161,132)
val pink = C("robe",new Color(176,33,76))
addStandingTorso(skin,pink,new Color(238,188,57))
addFace(skin,new Color(55,37,29),4.00,true,false,Some(new Color(207,32,45)))
addCrown(new Color(238,187,58),4.37)
// Lotus petals around the base.
for (i <- 0 until 12) {
val a = i * 2.0 * math.Pi / 12.0
addEllipsoid(V(0.67*math.cos(a),0.99,0.67*math.sin(a)),0.17,0.08,0.23,
new Color(232,109,153),10,14)
}
// Gold coins symbolising abundance.
for (i <- 0 until 5) {
addEllipsoid(V(0.60+i*0.08,2.04-i*0.12,0.44),0.035,0.035,0.025,new Color(255,209,66),8,10)
}
addEllipsoid(V(-0.78,2.25,0.36),0.14,0.18,0.12,new Color(237,173,56),10,14)
}
def buildVishnu(): Unit = {
val skin = new Color(82,139,206)
addStandingTorso(skin,new Color(236,189,54),new Color(52,126,77))
addFace(skin,new Color(48,35,26),4.00,true,false,Some(new Color(210,40,45)))
addCrown(new Color(228,177,51),4.37)
// Four arms and the conch/disc symbols.
addCylinder(V(-0.42,2.94,0),V(-0.90,3.26,0.02),0.12,0.08,new Color(235,184,50),10,true)
addEllipsoid(V(-0.94,3.29,0.04),0.13,0.13,0.11,new Color(246,226,184),10,12)
addCylinder(V(0.42,2.94,0),V(0.90,3.26,0.02),0.12,0.08,new Color(235,184,50),10,true)
addTorusXY(0.94,3.29,0.16,0.12,0.025,new Color(228,182,61),24,6)
addCylinder(V(-0.42,2.55,0),V(-0.96,2.30,0.02),0.12,0.08,new Color(235,184,50),10,true)
addEllipsoid(V(-1.01,2.28,0.03),0.14,0.14,0.10,new Color(231,224,204),10,12)
addCylinder(V(0.42,2.55,0),V(0.96,2.30,0.02),0.12,0.08,new Color(235,184,50),10,true)
addEllipsoid(V(1.01,2.28,0.04),0.11,0.16,0.09,new Color(224,180,53),10,12)
}
def buildMahavir(): Unit = {
val skin = new Color(206,158,124)
val robe = C("robe",new Color(226,220,202))
addEllipsoid(V(0,1.31,0.06),0.73,0.28,0.45,robe,16,22)
addEllipsoid(V(-0.38,1.28,0.18),0.40,0.20,0.38,robe,12,18)
addEllipsoid(V(0.38,1.28,0.18),0.40,0.20,0.38,robe,12,18)
addEllipsoid(V(0,1.91,0.01),0.50,0.44,0.35,robe,16,22)
addEllipsoid(V(0,2.44,0.00),0.51,0.60,0.34,robe,16,22)
addCylinder(V(-0.42,2.58,0),V(-0.26,2.03,0.35),0.14,0.11,robe,12,true)
addCylinder(V(0.42,2.58,0),V(0.26,2.03,0.35),0.14,0.11,robe,12,true)
addEllipsoid(V(-0.12,1.99,0.39),0.20,0.08,0.10,skin,10,14)
addEllipsoid(V(0.12,1.99,0.39),0.20,0.08,0.10,skin,10,14)
addFace(skin,new Color(64,47,35),3.48,false,true,None)
addEllipsoid(V(0,3.89,0.04),0.18,0.14,0.17,skin,10,14)
}
def buildSaiBaba(): Unit = {
val skin = new Color(196,145,112)
val robe = C("robe",new Color(231,227,212))
addEllipsoid(V(0,1.48,0.05),0.55,0.48,0.39,robe,16,20)
addEllipsoid(V(0,2.08,0.00),0.51,0.62,0.35,robe,16,22)
addCylinder(V(-0.42,2.42,0),V(-0.30,1.92,0.36),0.14,0.10,robe,12,true)
addCylinder(V(0.42,2.42,0),V(0.30,1.92,0.36),0.14,0.10,robe,12,true)
addEllipsoid(V(0,1.88,0.39),0.26,0.10,0.10,skin,12,16)
addFace(skin,new Color(114,78,53),3.34,false,false,None)
// Head cloth, moustache and beard.
addEllipsoid(V(0,3.70,0.00),0.35,0.16,0.31,robe,14,18)
addEllipsoid(V(0,3.08,0.28),0.20,0.23,0.12,new Color(166,155,141),12,16)
addCylinder(V(-0.15,3.23,0.30),V(0.0,3.18,0.33),0.03,0.02,new Color(91,66,49),8,true)
addCylinder(V(0.15,3.23,0.30),V(0.0,3.18,0.33),0.03,0.02,new Color(91,66,49),8,true)
}
def buildJesus(): Unit = {
val skin = new Color(195,146,118)
addStandingTorso(skin,new Color(228,218,190),new Color(153,35,46))
addFace(skin,new Color(88,48,31),4.00,true,false,None)
addEllipsoid(V(0,3.72,0.08),0.12,0.24,0.10,new Color(94,54,35),10,14)
addEllipsoid(V(-0.15,3.82,0.05),0.08,0.25,0.08,new Color(94,54,35),10,14)
addEllipsoid(V(0.15,3.82,0.05),0.08,0.25,0.08,new Color(94,54,35),10,14)
// Simple wooden cross behind the figure.
addBox(0.93,2.70,-0.42,0.11,2.35,0.10,new Color(104,66,40))
addBox(0.93,3.25,-0.40,0.95,0.12,0.11,new Color(104,66,40))
}
def buildGuruNanak(): Unit = {
val skin = new Color(197,151,121)
val robe = C("robe",new Color(224,195,132))
addEllipsoid(V(0,1.47,0.03),0.58,0.44,0.37,robe,16,20)
addEllipsoid(V(0,2.12,0),0.53,0.66,0.36,robe,16,22)
addCylinder(V(-0.42,2.48,0),V(-0.27,1.95,0.35),0.14,0.11,robe,12,true)
addCylinder(V(0.42,2.48,0),V(0.27,1.95,0.35),0.14,0.11,robe,12,true)
addEllipsoid(V(0,1.92,0.39),0.25,0.10,0.10,skin,12,16)
addFace(skin,new Color(91,60,39),3.39,false,false,None)
addEllipsoid(V(0,3.72,0.00),0.36,0.20,0.32,new Color(233,225,203),14,18) // turban
addEllipsoid(V(0,3.20,0.29),0.19,0.22,0.12,new Color(158,139,112),12,16) // beard
addCylinder(V(-0.16,3.26,0.31),V(0,3.20,0.34),0.025,0.02,new Color(90,67,48),8,true)
addCylinder(V(0.16,3.26,0.31),V(0,3.20,0.34),0.025,0.02,new Color(90,67,48),8,true)
}
def buildOtherFigure(): Unit = {
selectedFigure match {
case "Gautam Buddha" => buildBuddha()
case "Ganesh Ji" => buildGanesh()
case "Lord Shiva" => buildShiva()
case "Shri Krishna" => buildKrishna()
case "Lord Rama" => buildRama()
case "Hanuman Ji" => buildHanuman()
case "Maa Durga" => buildDurga()
case "Maa Saraswati" => buildSaraswati()
case "Maa Lakshmi" => buildLakshmi()
case "Lord Vishnu" => buildVishnu()
case "Mahavir Swami" => buildMahavir()
case "Sai Baba" => buildSaiBaba()
case "Jesus Christ" => buildJesus()
case "Guru Nanak Dev Ji" => buildGuruNanak()
case _ => buildBuddha()
}
}
// --------------------------------------------------------------
// SOFTWARE 3D RENDERER: near clipping + perspective + Z-buffer.
// --------------------------------------------------------------
val nearPlane = 0.18
def transform(p: V): TV = {
val cy = math.cos(yaw); val sy = math.sin(yaw)
val x1 = p.x * cy - p.z * sy
val z1 = p.x * sy + p.z * cy
val y1 = p.y - targetY
val cp = math.cos(pitch); val sp = math.sin(pitch)
val y2 = y1 * cp - z1 * sp
val z2 = y1 * sp + z1 * cp
TV(x1, y2, zoom - z2)
}
def clipNear(poly: scala.collection.immutable.List[TV]): scala.collection.immutable.List[TV] = {
if (poly.isEmpty) return poly
val out = ArrayBuffer[TV]()
var previous = poly.last
var previousInside = previous.z > nearPlane
poly.foreach { current =>
val currentInside = current.z > nearPlane
if (currentInside != previousInside) {
val den = current.z - previous.z
if (math.abs(den) > 0.0000001) {
val t = (nearPlane - previous.z) / den
out += TV(
previous.x + (current.x - previous.x) * t,
previous.y + (current.y - previous.y) * t,
nearPlane
)
}
}
if (currentInside) out += current
previous = current
previousInside = currentInside
}
out.toList
}
def project(p: TV, w: Int, h: Int): SV = {
val focal = math.min(w, h) * 1.03
SV(w / 2.0 + p.x * focal / p.z,
h / 2.0 - p.y * focal / p.z,
1.0 / p.z)
}
def rasterTriangle(imgPixels: Array[Int], zbuf: Array[Double],
w: Int, h: Int, a: SV, b: SV, c: SV, color: Color): Unit = {
val area = (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y)
if (math.abs(area) < 0.00001) return
val minX = math.max(0, math.floor(math.min(a.x, math.min(b.x,c.x))).toInt)
val maxX = math.min(w - 1, math.ceil(math.max(a.x, math.max(b.x,c.x))).toInt)
val minY = math.max(0, math.floor(math.min(a.y, math.min(b.y,c.y))).toInt)
val maxY = math.min(h - 1, math.ceil(math.max(a.y, math.max(b.y,c.y))).toInt)
if (minX > maxX || minY > maxY) return
val rgb = 0xff000000 | (color.getRed << 16) | (color.getGreen << 8) | color.getBlue
var y = minY
while (y <= maxY) {
val py = y + 0.5
var x = minX
while (x <= maxX) {
val px = x + 0.5
val wa = ((b.y-c.y)*(px-c.x) + (c.x-b.x)*(py-c.y)) / area
val wb = ((c.y-a.y)*(px-c.x) + (a.x-c.x)*(py-c.y)) / area
val wc = 1.0 - wa - wb
if (wa >= -0.00001 && wb >= -0.00001 && wc >= -0.00001) {
val inv = wa*a.invZ + wb*b.invZ + wc*c.invZ
val idx = y*w + x
if (inv > zbuf(idx)) {
zbuf(idx) = inv
imgPixels(idx) = rgb
}
}
x += 1
}
y += 1
}
}
def renderImage(w0: Int, h0: Int): BufferedImage = {
val w = math.max(300,w0); val h = math.max(300,h0)
val img = new BufferedImage(w,h,BufferedImage.TYPE_INT_ARGB)
val bg = img.createGraphics()
bg.setPaint(new GradientPaint(0,0,backgroundTop,0,h,backgroundBottom))
bg.fillRect(0,0,w,h)
// A subtle floor horizon glow.
bg.setColor(new Color(174,113,43,22))
bg.fillOval(w/2 - 340,h/2 + 100,680,300)
bg.dispose()
val pixels = img.getRaster.getDataBuffer.asInstanceOf[DataBufferInt].getData
val zbuf = Array.fill(w*h)(0.0)
triangles.foreach { t =>
val av = transform(t.a); val bv = transform(t.b); val cv = transform(t.c)
val clipped = clipNear(scala.collection.immutable.List(av,bv,cv))
if (clipped.length >= 3) {
val first = project(clipped.head,w,h)
var i = 1
while (i < clipped.length - 1) {
rasterTriangle(pixels,zbuf,w,h,first,
project(clipped(i),w,h),project(clipped(i+1),w,h),t.color)
i += 1
}
}
}
val g = img.createGraphics()
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON)
drawOverlay(g,w,h)
g.dispose()
img
}
def projectWorld(x: Double,y: Double,z: Double,w: Int,h: Int): Point = {
val p = transform(V(x,y,z))
if (p.z <= nearPlane) new Point(w/2,h/2)
else {
val s = project(p,w,h)
new Point(s.x.toInt,s.y.toInt)
}
}
def drawOverlay(g: Graphics2D,w: Int,h: Int): Unit = {
// Header and soft dark band.
g.setColor(new Color(4,8,22,168))
g.fillRoundRect(16,14,w-32,66,22,22)
g.setFont(new Font("Serif",Font.BOLD,26))
g.setColor(new Color(255,218,140))
val title = selectedFigure.toUpperCase + " 3D STATUE"
g.drawString(title,(w-g.getFontMetrics.stringWidth(title))/2,43)
g.setFont(new Font("SansSerif",Font.PLAIN,13))
g.setColor(new Color(235,238,250))
val subtitle = "3D STATUE STUDIO | DRAG TO ROTATE | SCROLL TO ZOOM"
g.drawString(subtitle,(w-g.getFontMetrics.stringWidth(subtitle))/2,64)
// Label over the front face of the pedestal when viewed near-front-on.
if (math.abs(yaw) < 0.55 || math.abs(yaw - 2*math.Pi) < 0.55) {
val p = projectWorld(0,0.42,0.81,w,h)
g.setFont(new Font("Serif",Font.BOLD,10))
g.setColor(new Color(255,221,142))
val txt = if (selectedFigure == "Dr. Babasaheb Ambedkar") "DR. B. R. AMBEDKAR" else selectedFigure.toUpperCase
g.drawString(txt,p.x-g.getFontMetrics.stringWidth(txt)/2,p.y+4)
}
// Bottom help strip.
g.setColor(new Color(3,6,17,185))
g.fillRoundRect(18,h-47,w-36,30,14,14)
g.setFont(new Font("SansSerif",Font.PLAIN,12))
g.setColor(new Color(237,240,250))
val help = "Mouse drag: rotate | Wheel / +/-: zoom | A/D: rotate | Arrows: tilt | 1-4: finishes | H: halo | B: book | R: reset"
g.drawString(help,30,h-28)
}
// --------------------------------------------------------------
// UI, themes and screenshot export.
// --------------------------------------------------------------
var canvas: JPanel = _
var statusLabel: JLabel = _
var frame: JFrame = _
def resetCamera(): Unit = {
yaw = 0.0
pitch = 0.02
zoom = 9.4
if (canvas != null) canvas.repaint()
}
def saveScreenshot(): Unit = {
val chooser = new JFileChooser()
chooser.setDialogTitle("Save statue screenshot as PNG")
chooser.setSelectedFile(new File(selectedFigure.replaceAll("[^A-Za-z0-9]+", "_") + "_3D_Statue.png"))
if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
try {
val image = renderImage(canvas.getWidth,canvas.getHeight)
val target = chooser.getSelectedFile
val file = if (target.getName.toLowerCase.endsWith(".png")) target
else new File(target.getParentFile,target.getName + ".png")
ImageIO.write(image,"png",file)
statusLabel.setText("Saved: " + file.getAbsolutePath)
} catch {
case ex: Exception =>
JOptionPane.showMessageDialog(frame,"Could not save screenshot: " + ex.getMessage,
"Save Error",JOptionPane.ERROR_MESSAGE)
}
}
}
def makeButton(text: String): JButton = {
val b = new JButton(text)
b.setFont(new Font("SansSerif",Font.BOLD,13))
b.setFocusPainted(false)
b.setBackground(new Color(28,43,81))
b.setForeground(new Color(245,240,225))
b.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(207,164,75),1),
BorderFactory.createEmptyBorder(7,10,7,10)))
b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
b
}
def start(): Unit = {
buildScene()
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
frame = new JFrame(selectedFigure + " - 3D Figure Studio - Scala")
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
frame.setMinimumSize(new Dimension(800,650))
val root = new JPanel(new BorderLayout(0,0))
root.setBackground(new Color(8,12,28))
val tools = new JPanel(new GridLayout(2,1,2,2))
tools.setBackground(new Color(13,20,43))
val figureBar = new JPanel(new FlowLayout(FlowLayout.CENTER,8,4))
figureBar.setBackground(new Color(13,20,43))
val buttonBar = new JPanel(new FlowLayout(FlowLayout.CENTER,8,4))
buttonBar.setBackground(new Color(13,20,43))
val figureBox = new JComboBox[String](figureNames)
figureBox.setSelectedItem(selectedFigure)
figureBox.setFont(new Font("SansSerif",Font.BOLD,13))
figureBox.setPreferredSize(new Dimension(245,32))
figureBox.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
selectedFigure = figureBox.getSelectedItem.toString
buildScene()
resetCamera()
canvas.repaint()
frame.setTitle(selectedFigure + " - 3D Figure Studio")
statusLabel.setText(" Selected: " + selectedFigure + " ? use the mouse to rotate and inspect the model.")
}
})
val themeBox = new JComboBox[String](Array(
"Classic Colours", "Bronze Sculpture", "Golden Tribute", "White Marble"
))
themeBox.setFont(new Font("SansSerif",Font.BOLD,13))
themeBox.setPreferredSize(new Dimension(160,32))
themeBox.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
themeMode = themeBox.getSelectedIndex
buildScene()
canvas.repaint()
}
})
val front = makeButton("FRONT VIEW")
front.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = resetCamera()
})
val halo = makeButton("TOGGLE HALO (H)")
halo.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
showHalo = !showHalo; buildScene(); canvas.repaint()
statusLabel.setText("Halo: " + (if (showHalo) "ON" else "OFF"))
}
})
val book = makeButton("TOGGLE BOOK (B)")
book.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
showBook = !showBook; buildScene(); canvas.repaint()
statusLabel.setText("Book toggle applies to Dr. Babasaheb Ambedkar's Constitution prop.")
}
})
val shot = makeButton("SAVE PNG")
shot.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = saveScreenshot()
})
val figureLabel = new JLabel("CLICK TO CHOOSE FIGURE:")
figureLabel.setForeground(new Color(242,222,173))
val themeLabel = new JLabel("COLOUR FINISH:")
themeLabel.setForeground(new Color(242,222,173))
figureBar.add(figureLabel)
figureBar.add(figureBox)
figureBar.add(themeLabel)
figureBar.add(themeBox)
buttonBar.add(front); buttonBar.add(halo); buttonBar.add(book); buttonBar.add(shot)
tools.add(figureBar)
tools.add(buttonBar)
root.add(tools,BorderLayout.NORTH)
canvas = new JPanel() {
setOpaque(true)
setBackground(Color.BLACK)
setFocusable(true)
override def paintComponent(g: Graphics): Unit = {
super.paintComponent(g)
val image = renderImage(getWidth,getHeight)
g.drawImage(image,0,0,null)
}
}
root.add(canvas,BorderLayout.CENTER)
statusLabel = new JLabel(" Ready ? choose a figure above, then drag the model to inspect it from every side.")
statusLabel.setFont(new Font("SansSerif",Font.PLAIN,12))
statusLabel.setForeground(new Color(230,230,245))
statusLabel.setBorder(BorderFactory.createEmptyBorder(7,12,7,12))
statusLabel.setOpaque(true)
statusLabel.setBackground(new Color(10,15,31))
root.add(statusLabel,BorderLayout.SOUTH)
var lastX = 0
var lastY = 0
canvas.addMouseListener(new MouseAdapter {
override def mousePressed(e: MouseEvent): Unit = {
lastX = e.getX; lastY = e.getY
canvas.requestFocusInWindow()
}
})
canvas.addMouseMotionListener(new MouseMotionAdapter {
override def mouseDragged(e: MouseEvent): Unit = {
val dx = e.getX - lastX
val dy = e.getY - lastY
yaw += dx * 0.009
pitch = clamp(pitch + dy * 0.006,-0.72,0.72)
lastX = e.getX; lastY = e.getY
canvas.repaint()
}
})
canvas.addMouseWheelListener(new MouseWheelListener {
override def mouseWheelMoved(e: MouseWheelEvent): Unit = {
zoom = clamp(zoom + e.getWheelRotation * 0.45,6.3,15.5)
canvas.repaint()
}
})
canvas.addKeyListener(new KeyAdapter {
override def keyPressed(e: KeyEvent): Unit = {
e.getKeyCode match {
case KeyEvent.VK_A | KeyEvent.VK_LEFT => yaw -= 0.12
case KeyEvent.VK_D | KeyEvent.VK_RIGHT => yaw += 0.12
case KeyEvent.VK_UP => pitch = clamp(pitch - 0.06,-0.72,0.72)
case KeyEvent.VK_DOWN => pitch = clamp(pitch + 0.06,-0.72,0.72)
case KeyEvent.VK_ADD | KeyEvent.VK_EQUALS => zoom = clamp(zoom - 0.4,6.3,15.5)
case KeyEvent.VK_SUBTRACT | KeyEvent.VK_MINUS => zoom = clamp(zoom + 0.4,6.3,15.5)
case KeyEvent.VK_H => showHalo = !showHalo; buildScene()
case KeyEvent.VK_B => showBook = !showBook; buildScene()
case KeyEvent.VK_R => resetCamera()
case KeyEvent.VK_1 => themeMode = 0; themeBox.setSelectedIndex(0); buildScene()
case KeyEvent.VK_2 => themeMode = 1; themeBox.setSelectedIndex(1); buildScene()
case KeyEvent.VK_3 => themeMode = 2; themeBox.setSelectedIndex(2); buildScene()
case KeyEvent.VK_4 => themeMode = 3; themeBox.setSelectedIndex(3); buildScene()
case _ => ()
}
canvas.repaint()
}
})
frame.setContentPane(root)
frame.setSize(1220,880)
frame.setLocationRelativeTo(null)
frame.setVisible(true)
canvas.requestFocusInWindow()
}
})
}
}
// ============================================================
// CRICKET LIVE CENTER - integrated app module
// ============================================================
import java.net.{HttpURLConnection, URI, URL}
import java.io.{BufferedReader, InputStreamReader}
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.Date
import javax.swing.table.DefaultTableModel
import scala.collection.mutable.ArrayBuffer
object YTRCricketLiveCenterApp {
private val APP_TITLE = "CRICKET LIVE CENTER"
private val HOME_URL = "https://www.cricbuzz.com/cricket-match/live-scores"
private val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/153.0 Safari/537.36"
private val LOGIN_SALT = "YTR-LAPTOP-LOGIN-SALT-2026"
private val LOGIN_HASH = "2f52b3afad1d7edfe3df7406a24c03d0855be3b0ba69e8108bb839cd03944940"
private val AUTO_REFRESH_MS = 20000
private val bg = new Color(10,14,24)
private val panel = new Color(18,24,38)
private val panel2 = new Color(24,31,48)
private val white = new Color(245,247,250)
private val muted = new Color(160,169,184)
private val green = new Color(45,190,120)
private val blue = new Color(55,145,255)
private val orange = new Color(242,159,67)
private val border = new Color(49,60,82)
private case class MatchInfo(name: String, url: String, status: String) {
override def toString: String = status + " | " + name
}
private def sha256(text: String): String = {
val digest = java.security.MessageDigest.getInstance("SHA-256")
digest.digest(text.getBytes(StandardCharsets.UTF_8)).map(b => "%02x".format(b & 0xff)).mkString
}
private def verify(password: String): Boolean = sha256(LOGIN_SALT + password) == LOGIN_HASH
private def fetch(urlText: String): String = {
var connection: HttpURLConnection = null
var reader: BufferedReader = null
try {
connection = new URL(urlText).openConnection().asInstanceOf[HttpURLConnection]
connection.setRequestMethod("GET")
connection.setRequestProperty("User-Agent", USER_AGENT)
connection.setRequestProperty("Accept", "text/html,application/xhtml+xml")
connection.setConnectTimeout(8000)
connection.setReadTimeout(12000)
connection.setInstanceFollowRedirects(true)
val code = connection.getResponseCode
if (code < 200 || code >= 400) throw new RuntimeException("HTTP " + code)
reader = new BufferedReader(new InputStreamReader(connection.getInputStream, StandardCharsets.UTF_8))
val out = new StringBuilder
var line = reader.readLine()
var total = 0
while (line != null && total < 1800000) { out.append(line).append("\n"); total += line.length; line = reader.readLine() }
out.toString
} finally {
if (reader != null) try reader.close() catch { case _: Throwable => () }
if (connection != null) connection.disconnect()
}
}
private def cleanHtml(html: String): String = {
if (html == null) return ""
html.replaceAll("(?is)<script[^>]*>.*?</script>", " ")
.replaceAll("(?is)<style[^>]*>.*?</style>", " ")
.replaceAll("(?is)<[^>]+>", " ")
.replace(" ", " ").replace("&", "&").replace(""", String.valueOf('"'))
.replace("'", "'").replace("<", "<").replace(">", ">")
.replaceAll("\\s+", " ").trim
}
private def parseMatches(html: String): Vector[MatchInfo] = {
val out = ArrayBuffer[MatchInfo]()
val pattern = """(?is)<a[^>]+href=["']([^"']*live-cricket-(?:scores|scorecard)[^"']*)["'][^>]*>(.*?)</a>""".r
pattern.findAllMatchIn(Option(html).getOrElse("")).foreach { m =>
val href = m.group(1)
val name = cleanHtml(m.group(2))
if (name.length >= 6) {
val absolute = if (href.startsWith("http")) href else "https://www.cricbuzz.com" + (if (href.startsWith("/")) href else "/" + href)
val low = name.toLowerCase
val status = if (low.contains("live")) "LIVE" else if (low.contains("won") || low.contains("result")) "RECENT" else if (low.contains("tomorrow") || low.contains("upcoming")) "UPCOMING" else "MATCH"
if (!out.exists(_.url == absolute)) out += MatchInfo(name.take(90), absolute, status)
}
}
if (out.isEmpty) Vector(MatchInfo("Cricbuzz Live Scores (open source)", HOME_URL, "LIVE")) else out.take(100).toVector
}
private def scoreSummary(html: String): (String,String,String,String,String) = {
val text = cleanHtml(html)
val scorePattern = """(\d{1,4}/\d{1,2})\s*\((\d{1,3}(?:\.\d+)?)\s*ov\)""".r
val found = scorePattern.findFirstMatchIn(text)
val score = found.map(m => m.group(1) + " (" + m.group(2) + " ov)").getOrElse("Score not available in public HTML")
val crrPat = """(?i)(?:current run rate|crr)\s*[:\-]?\s*(\d+(?:\.\d+)?)""".r
val reqPat = """(?i)(?:required run rate|req(?:uired)?\.?\s*rr)\s*[:\-]?\s*(\d+(?:\.\d+)?)""".r
val targetPat = """(?i)target\s*[:\-]?\s*(\d{1,4})""".r
val venuePat = """(?i)(?:venue|ground|stadium)\s*[:\-]\s*([A-Za-z0-9 ,'.()&-]{5,100})""".r
(score, crrPat.findFirstMatchIn(text).map(_.group(1)).getOrElse("?"), reqPat.findFirstMatchIn(text).map(_.group(1)).getOrElse("?"), targetPat.findFirstMatchIn(text).map(_.group(1)).getOrElse("?"), venuePat.findFirstMatchIn(text).map(_.group(1).take(100)).getOrElse("?"))
}
private def styledLabel(text: String, size: Int, bold: Boolean = false, color: Color = white): JLabel = {
val label = new JLabel(text)
label.setForeground(color)
label.setFont(new Font("Arial", if (bold) Font.BOLD else Font.PLAIN, size))
label
}
private def appButton(text: String): JButton = {
val b = new JButton(text)
b.setFocusPainted(false)
b.setBackground(new Color(37,48,70))
b.setForeground(white)
b.setBorder(javax.swing.BorderFactory.createLineBorder(border))
b
}
def start(): Unit = SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
val frame = new JFrame(APP_TITLE + " - Secure Login")
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
frame.setSize(470,350)
frame.setLocationRelativeTo(null)
val root = new JPanel(new BorderLayout(8,8))
root.setBackground(bg)
root.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,28,20,28))
val title = styledLabel("CRICKET LIVE CENTER",24,true,white)
val subtitle = styledLabel("Live scores ? commentary ? scorecard ? analytics",12,false,muted)
val top = new JPanel(new java.awt.GridLayout(2,1,2,2)); top.setOpaque(false); top.add(title); top.add(subtitle)
val name = new JTextField("Yadnesh")
val pass = new JPasswordField()
name.setBackground(panel2); name.setForeground(white)
pass.setBackground(panel2); pass.setForeground(white)
val form = new JPanel(new java.awt.GridLayout(0,1,6,6)); form.setOpaque(false)
form.add(styledLabel("USERNAME",11,true,muted)); form.add(name)
form.add(styledLabel("PASSWORD",11,true,muted)); form.add(pass)
val msg = styledLabel("Password required",11,false,orange)
val login = appButton("LOGIN")
def doLogin(): Unit = {
if (verify(new String(pass.getPassword))) { frame.dispose(); new CricketWindow().setVisible(true) }
else { msg.setText("Wrong password"); pass.setText("") }
}
login.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = doLogin() })
pass.addActionListener(new ActionListener { override def actionPerformed(e: ActionEvent): Unit = doLogin() })
val bottom = new JPanel(new BorderLayout(6,6)); bottom.setOpaque(false); bottom.add(login,BorderLayout.CENTER); bottom.add(msg,BorderLayout.SOUTH)
root.add(top,BorderLayout.NORTH); root.add(form,BorderLayout.CENTER); root.add(bottom,BorderLayout.SOUTH)
frame.setContentPane(root); frame.setVisible(true)
}
})
private class CricketWindow extends JFrame(APP_TITLE) {
private val matchModel = new DefaultListModel[MatchInfo]()
private val matchList = new JList[MatchInfo](matchModel)
private val status = styledLabel("Loading Cricbuzz matches...",11,false,muted)
private val matchTitle = styledLabel("LIVE CRICKET",20,true,white)
private val score = styledLabel("?",42,true,white)
private val crr = styledLabel("CRR ?",14,true,white)
private val req = styledLabel("REQ ?",14,true,white)
private val target = styledLabel("TARGET ?",14,true,white)
private val venue = styledLabel("Venue: ?",12,false,muted)
private val updated = styledLabel("Updated: ?",11,false,muted)
private val commentary = new JTextArea()
commentary.setEditable(false); commentary.setLineWrap(true); commentary.setWrapStyleWord(true)
commentary.setBackground(panel2); commentary.setForeground(white); commentary.setFont(new Font("Monospaced",Font.PLAIN,12))
private val tableModel = new DefaultTableModel(Array[Object]("BATTER / PLAYER", "RUNS", "DETAIL"),0) { override def isCellEditable(r:Int,c:Int):Boolean=false }
private val table = new JTable(tableModel)
private val tabs = new JTabbedPane()
private var selected: MatchInfo = MatchInfo("Cricbuzz Live Scores",HOME_URL,"LIVE")
private val refresh = appButton("REFRESH NOW")
private val source = appButton("OPEN SOURCE")
private val timer = new javax.swing.Timer(AUTO_REFRESH_MS,new ActionListener { override def actionPerformed(e:ActionEvent):Unit = refreshSelected() })
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
setSize(1260,820); setMinimumSize(new Dimension(980,650)); setLocationRelativeTo(null)
private def buildUI(): Unit = {
val root = new JPanel(new BorderLayout(10,10)); root.setBackground(bg); root.setBorder(javax.swing.BorderFactory.createEmptyBorder(12,12,12,12))
val top = new JPanel(new BorderLayout()); top.setBackground(panel); top.setBorder(javax.swing.BorderFactory.createEmptyBorder(12,16,12,16))
val titleStack = new JPanel(new java.awt.GridLayout(2,1)); titleStack.setOpaque(false); titleStack.add(styledLabel("CRICKET LIVE CENTER",22,true,white)); titleStack.add(styledLabel("LIVE SCORES ? COMMENTARY ? SCORECARD ? ANALYTICS",11,false,muted))
val buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT)); buttons.setOpaque(false); buttons.add(refresh); buttons.add(source); top.add(titleStack,BorderLayout.WEST); top.add(buttons,BorderLayout.EAST)
val side = new JPanel(new BorderLayout(5,5)); side.setBackground(panel); side.setPreferredSize(new Dimension(330,100)); side.add(styledLabel("MATCHES",14,true,white),BorderLayout.NORTH)
matchList.setBackground(panel2); matchList.setForeground(white); matchList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
side.add(new JScrollPane(matchList),BorderLayout.CENTER); side.add(status,BorderLayout.SOUTH)
val overview = new JPanel(new BorderLayout(8,8)); overview.setBackground(bg); overview.setBorder(javax.swing.BorderFactory.createEmptyBorder(12,4,4,4))
val scoreBox = new JPanel(new BorderLayout(5,5)); scoreBox.setBackground(panel); scoreBox.setBorder(javax.swing.BorderFactory.createEmptyBorder(20,20,20,20))
scoreBox.add(matchTitle,BorderLayout.NORTH); scoreBox.add(score,BorderLayout.CENTER)
val stats = new JPanel(new FlowLayout(FlowLayout.LEFT,18,8)); stats.setBackground(panel); stats.add(crr); stats.add(req); stats.add(target)
val details = new JPanel(new java.awt.GridLayout(2,1,4,4)); details.setBackground(panel); details.setBorder(javax.swing.BorderFactory.createEmptyBorder(8,18,12,18)); details.add(venue); details.add(updated)
val south = new JPanel(new BorderLayout()); south.setBackground(panel); south.add(stats,BorderLayout.NORTH); south.add(details,BorderLayout.CENTER); scoreBox.add(south,BorderLayout.SOUTH); overview.add(scoreBox,BorderLayout.NORTH)
val overviewInfo = new JTextArea("""Select a match from the left and press REFRESH NOW.
Scores are shown only when successfully parsed from Cricbuzz's public webpage.
Use OPEN SOURCE to view the official source if the page does not expose scores to this simple reader.""")
overviewInfo.setEditable(false); overviewInfo.setLineWrap(true); overviewInfo.setWrapStyleWord(true); overviewInfo.setBackground(panel2); overviewInfo.setForeground(muted); overviewInfo.setBorder(javax.swing.BorderFactory.createEmptyBorder(12,12,12,12)); overview.add(new JScrollPane(overviewInfo),BorderLayout.CENTER)
tabs.addTab("OVERVIEW",overview)
table.setRowHeight(28); table.setBackground(panel2); table.setForeground(white); table.setGridColor(border); table.getTableHeader.setBackground(panel); table.getTableHeader.setForeground(muted)
tabs.addTab("SCORECARD",new JScrollPane(table))
tabs.addTab("COMMENTARY",new JScrollPane(commentary))
val analytics = new JPanel(new BorderLayout()); analytics.setBackground(panel2); analytics.add(styledLabel("RUN / SCORE SUMMARY ? refreshed from the selected source",14,true,white),BorderLayout.NORTH); analytics.add(styledLabel("This panel does not invent missing score data.",12,false,muted),BorderLayout.CENTER); tabs.addTab("ANALYTICS",analytics)
root.add(top,BorderLayout.NORTH); root.add(side,BorderLayout.WEST); root.add(tabs,BorderLayout.CENTER); setContentPane(root)
refresh.addActionListener(new ActionListener { override def actionPerformed(e:ActionEvent):Unit = { refreshMatchList(); refreshSelected() } })
source.addActionListener(new ActionListener { override def actionPerformed(e:ActionEvent):Unit = openSource() })
matchList.addListSelectionListener(new javax.swing.event.ListSelectionListener { override def valueChanged(e:javax.swing.event.ListSelectionEvent):Unit = if (!e.getValueIsAdjusting && matchList.getSelectedValue != null) { selected=matchList.getSelectedValue; matchTitle.setText(selected.name.toUpperCase); refreshSelected() } })
addWindowListener(new java.awt.event.WindowAdapter { override def windowClosed(e:java.awt.event.WindowEvent):Unit = timer.stop() })
}
private def refreshMatchList(): Unit = {
status.setText("Reading live match list...")
val worker = new SwingWorker[Vector[MatchInfo],Void] {
override def doInBackground():Vector[MatchInfo] = parseMatches(fetch(HOME_URL))
override def done():Unit = try { val matches=get(); val previous=selected.url; matchModel.clear(); matches.foreach(matchModel.addElement); val idx=matches.indexWhere(_.url==previous); val use=if(idx>=0) idx else 0; if(matches.nonEmpty) { matchList.setSelectedIndex(use); selected=matches(use) }; status.setText(matches.length+" matches ? "+new SimpleDateFormat("HH:mm:ss").format(new Date())) } catch { case ex:Throwable => if(matchModel.isEmpty) { matchModel.addElement(selected); matchList.setSelectedIndex(0) }; status.setText("Source unavailable: "+Option(ex.getMessage).getOrElse("network error")) }
}; worker.execute()
}
private def refreshSelected(): Unit = {
if (selected == null) return
status.setText("Refreshing selected match...")
val targetMatch = selected
val worker = new SwingWorker[String,Void] {
override def doInBackground():String = fetch(targetMatch.url)
override def done():Unit = try {
val html=get(); val summary=scoreSummary(html); score.setText(summary._1); crr.setText("CRR "+summary._2); req.setText("REQ "+summary._3); target.setText("TARGET "+summary._4); venue.setText("Venue: "+summary._5); updated.setText("Updated: "+new SimpleDateFormat("HH:mm:ss").format(new Date()))
val text=cleanHtml(html); commentary.setText(if(text.nonEmpty) text.take(12000) else "No readable commentary was returned by the source."); commentary.setCaretPosition(0)
tableModel.setRowCount(0); tableModel.addRow(Array[Object]("Score",summary._1,"Selected public webpage")); tableModel.addRow(Array[Object]("Current Run Rate",summary._2,"CRR")); tableModel.addRow(Array[Object]("Required Run Rate",summary._3,"REQ")); tableModel.addRow(Array[Object]("Target",summary._4,"If published"))
status.setText("Source read OK ? "+new SimpleDateFormat("HH:mm:ss").format(new Date()))
} catch { case ex:Throwable => score.setText("Live score unavailable"); commentary.setText("The public webpage could not be read or did not expose structured score data.\n\nOpen the official source using OPEN SOURCE.\n\nDetails: "+Option(ex.getMessage).getOrElse("network error")); status.setText("Refresh failed: "+Option(ex.getMessage).getOrElse("network error")) }
}; worker.execute()
}
private def openSource(): Unit = try { if (Desktop.isDesktopSupported) Desktop.getDesktop.browse(new URI(selected.url)) else JOptionPane.showMessageDialog(this,selected.url,"Source URL",JOptionPane.INFORMATION_MESSAGE) } catch { case _:Throwable => JOptionPane.showMessageDialog(this,selected.url,"Source URL",JOptionPane.INFORMATION_MESSAGE) }
buildUI()
timer.start()
refreshMatchList()
}
}
// ============================================================
// FINAL START - launch only the YTR simulated laptop
// ============================================================
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
launchUltraLaptop()
}
})