Code Sketch
granny yoi 3d
Category: Programming
import javax.swing._
import java.awt._
import java.awt.event._
import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable
object OldHouse3D {
// ==========================================================
// OLD HOUSE 3D
// Polygon-based 3D horror prototype
// Scala + Java Swing + software perspective renderer
// ==========================================================
case class V3(x: Double, y: Double, z: Double)
case class Box(
x: Double, y: Double, z: Double,
sx: Double, sy: Double, sz: Double,
color: Color,
solid: Boolean
)
case class Tri(a: V3, b: V3, c: V3, color: Color)
case class Cam(x: Double, y: Double, z: Double)
case class ScreenTri(
depth: Double,
xs: Array[Int],
ys: Array[Int],
color: Color
)
val worldTriangles = ArrayBuffer[Tri]()
val collisionBoxes = ArrayBuffer[Box]()
val keys = mutable.HashSet[Int]()
val WALL = new Color(75, 65, 56)
val WALL2 = new Color(105, 88, 71)
val FLOOR = new Color(70, 58, 47)
val WOOD = new Color(86, 52, 32)
val DARKWOOD = new Color(42, 29, 22)
val METAL = new Color(100, 104, 108)
val RED = new Color(85, 18, 20)
val SW = 1100
val SH = 680
var px = 3.2
var pz = 4.2
var yaw = math.Pi
var pitch = 0.0
var day = 1
var started = false
var gameOver = false
var escaped = false
var hidden = false
var eyeRise = 0.0
var flashlight = true
var showMap = false
var hasKey = false
var doorOpen = false
val keyX = 9.4
val keyZ = 3.3
val doorX = 20.5
val doorZ = 17.65
val wardrobeX = 1.25
val wardrobeZ = 4.8
val vaseX = 4.7
val vaseZ = 2.4
var vaseY = 1.05
var vaseVelocity = 0.0
var vaseFalling = false
var vaseBroken = false
var noiseX = vaseX
var noiseZ = vaseZ
var noiseTimer = 0.0
var enemyX = 21.0
var enemyZ = 3.0
var waypointX = enemyX
var waypointZ = enemyZ
var aiClock = 0.0
var patrolIndex = 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 = _
val GRID = 0.5
val GW = 48
val GH = 36
// ----------------------------------------------------------
// 3D MODEL CREATION
// ----------------------------------------------------------
def shade(c: Color, f: Double): Color = {
new Color(
math.max(0, math.min(255, (c.getRed * f).toInt)),
math.max(0, math.min(255, (c.getGreen * f).toInt)),
math.max(0, math.min(255, (c.getBlue * f).toInt))
)
}
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 light = Array(0.66, 1.0, 0.62, 0.82, 1.10, 0.40)
val out = ArrayBuffer[Tri]()
for (i <- faces.indices) {
val f = faces(i)
val col = shade(b.color, light(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)
}
out
}
def addBox(
x: Double, y: Double, z: Double,
sx: Double, sy: Double, sz: Double,
col: Color,
solid: Boolean = true
): Unit = {
val b = Box(x, y, z, sx, sy, sz, col, solid)
worldTriangles ++= boxTriangles(b)
if (solid) collisionBoxes += b
}
def dynamicBox(
out: ArrayBuffer[Tri],
x: Double, y: Double, z: Double,
sx: Double, sy: Double, sz: Double,
col: Color
): Unit = {
out ++= boxTriangles(
Box(x, y, z, sx, sy, sz, col, false)
)
}
// ----------------------------------------------------------
// HOUSE WALLS
// ----------------------------------------------------------
def wallX(
x: Double,
z0: Double,
z1: Double,
gaps: Seq[(Double, Double)]
): Unit = {
var cursor = z0
for ((a, b) <- gaps.sortBy(_._1)) {
if (a > cursor) {
addBox(
x, 1.5, (cursor + a) / 2,
0.22, 3.0, a - cursor,
WALL
)
}
cursor = math.max(cursor, b)
}
if (cursor < z1) {
addBox(
x, 1.5, (cursor + z1) / 2,
0.22, 3.0, z1 - cursor,
WALL
)
}
}
def wallZ(
z: Double,
x0: Double,
x1: Double,
gaps: Seq[(Double, Double)]
): Unit = {
var cursor = x0
for ((a, b) <- gaps.sortBy(_._1)) {
if (a > cursor) {
addBox(
(cursor + a) / 2, 1.5, z,
a - cursor, 3.0, 0.22,
WALL
)
}
cursor = math.max(cursor, b)
}
if (cursor < x1) {
addBox(
(cursor + x1) / 2, 1.5, z,
x1 - cursor, 3.0, 0.22,
WALL
)
}
}
def buildHouse(): Unit = {
worldTriangles.clear()
collisionBoxes.clear()
// Main floor
addBox(
12.0, -0.18, 9.0,
24.0, 0.36, 18.0,
FLOOR, false
)
// Outer walls
addBox(0.1, 1.5, 9.0, 0.3, 3.0, 18.0, WALL)
addBox(23.9, 1.5, 9.0, 0.3, 3.0, 18.0, WALL)
wallZ(0.1, 0.1, 23.9, Seq())
// Exit opening on the front wall
wallZ(18.0, 0.25, 23.75, Seq((19.7, 21.3)))
// Room partitions and door openings
wallX(
6.0, 0.25, 17.75,
Seq((2.4, 3.6), (8.2, 9.4), (14.0, 15.2))
)
wallX(
12.0, 0.25, 17.75,
Seq((3.0, 4.2), (8.0, 9.2), (14.6, 15.8))
)
wallZ(
6.0, 0.25, 23.75,
Seq((3.5, 4.8), (8.0, 9.2), (17.5, 18.7))
)
wallZ(
12.0, 0.25, 23.75,
Seq((2.0, 3.2), (8.5, 9.7), (15.5, 16.7),
(19.9, 21.3))
)
// BEDROOM: bed frame, mattress and pillow
addBox(2.3, 0.28, 2.1, 2.25, 0.40, 2.9, DARKWOOD)
addBox(2.3, 0.58, 2.1, 2.05, 0.23, 2.68,
new Color(105, 100, 92))
addBox(2.3, 0.73, 0.98, 1.1, 0.12, 0.48,
new Color(195, 187, 170))
// Bed legs
for (x <- Seq(1.35, 3.25); z <- Seq(0.85, 3.25)) {
addBox(x, 0.14, z, 0.17, 0.28, 0.17, DARKWOOD)
}
// Nightstand
addBox(4.75, 0.43, 2.3, 0.8, 0.84, 0.8, WOOD)
addBox(4.75, 0.88, 2.3, 0.88, 0.10, 0.88, DARKWOOD)
// Wardrobe
addBox(
wardrobeX, 1.10, wardrobeZ,
0.95, 2.2, 0.9,
new Color(61, 40, 29)
)
addBox(
1.70, 1.10, wardrobeZ,
0.10, 2.05, 0.86,
new Color(83, 53, 37)
)
// Dining table
addBox(9.0, 0.68, 5.0, 1.6, 0.16, 1.2, WOOD)
for (x <- Seq(8.35, 9.65); z <- Seq(4.55, 5.45)) {
addBox(x, 0.32, z, 0.13, 0.64, 0.13, DARKWOOD)
}
// Cupboards in other rooms
addBox(15.0, 0.90, 2.0, 1.0, 1.8, 0.65, DARKWOOD)
addBox(18.5, 0.95, 10.3, 1.1, 1.9, 0.9, WOOD)
addBox(4.0, 0.95, 14.8, 1.1, 1.9, 0.9, DARKWOOD)
// Tables and room furniture
addBox(17.1, 0.45, 4.5, 1.5, 0.12, 1.0, WOOD)
addBox(18.2, 0.37, 14.0, 1.25, 0.75, 0.8, DARKWOOD)
// 3D stairs: each step is real polygonal geometry.
for (i <- 0 until 10) {
val height = (i + 1) * 0.14
addBox(
15.8, height / 2,
7.2 + i * 0.34,
3.0, height, 0.36,
new Color(85, 62, 46),
false
)
}
// Stair railing
addBox(14.25, 0.75, 8.9, 0.12, 1.5, 3.5, DARKWOOD)
addBox(17.35, 0.75, 8.9, 0.12, 1.5, 3.5, DARKWOOD)
// Windows, represented by dark glass and frames
addBox(3.0, 2.0, 0.23, 1.5, 0.9, 0.08,
new Color(28, 43, 51), false)
addBox(14.0, 2.0, 0.23, 1.5, 0.9, 0.08,
new Color(28, 43, 51), false)
// A few wall decorations
addBox(10.0, 2.0, 0.23, 0.65, 0.75, 0.08,
new Color(42, 31, 24), false)
addBox(10.0, 2.0, 0.17, 0.52, 0.61, 0.04,
new Color(85, 74, 60), false)
}
// ----------------------------------------------------------
// PLAYER, CAMERA AND COLLISION
// ----------------------------------------------------------
def clamp(v: Double, lo: Double, hi: Double): Double =
math.max(lo, math.min(hi, v))
def distance(
x1: Double, z1: Double,
x2: Double, z2: Double
): Double = {
math.sqrt(
(x1 - x2) * (x1 - x2) +
(z1 - z2) * (z1 - z2)
)
}
def floorHeight(x: Double, z: Double): Double = {
if (x >= 14.3 && x <= 17.3 && z >= 7.0 && z <= 10.5) {
1.4 * clamp((z - 7.0) / 3.5, 0.0, 1.0)
} else {
0.0
}
}
def blocked(x: Double, z: Double, radius: Double): Boolean = {
if (x < 0.35 || x > 23.65 || z < 0.35) return true
// The front exit is traversable after its door is unlocked.
if (z > 17.65 && !(doorOpen && x > 19.4 && x < 21.6)) {
return true
}
if (z > 19.0) return true
if (!doorOpen &&
math.abs(x - doorX) < 0.72 + radius &&
math.abs(z - doorZ) < 0.23 + radius) {
return true
}
collisionBoxes.exists { b =>
math.abs(x - b.x) < b.sx / 2 + radius &&
math.abs(z - b.z) < b.sz / 2 + radius
}
}
def movePlayer(dx: Double, dz: Double): Unit = {
val radius = 0.20
if (!blocked(px + dx, pz, radius)) px += dx
if (!blocked(px, pz + dz, radius)) pz += dz
px = clamp(px, 0.4, 23.6)
}
def wake(): Unit = {
started = true
eyeRise = 0.0
message = "DAY " + day + " - FIND A WAY OUT"
messageTimer = 4.0
}
def resetGame(): Unit = {
day = 1
started = false
gameOver = false
escaped = false
hasKey = false
doorOpen = false
hidden = false
px = 3.2
pz = 4.2
yaw = math.Pi
pitch = 0.0
eyeRise = 0.0
flashlight = true
showMap = false
vaseY = 1.05
vaseVelocity = 0.0
vaseFalling = false
vaseBroken = false
noiseTimer = 0.0
enemyX = 21.0
enemyZ = 3.0
waypointX = enemyX
waypointZ = enemyZ
message = "DAY 1: PRESS ENTER TO WAKE UP"
messageTimer = 5.0
}
// ----------------------------------------------------------
// INTERACTIONS
// ----------------------------------------------------------
def say(s: String, seconds: Double = 2.5): Unit = {
message = s
messageTimer = seconds
}
def interact(): Unit = {
if (!started || gameOver || escaped) return
if (distance(px, pz, doorX, doorZ - 0.7) < 1.35) {
if (hasKey) {
doorOpen = true
say("EXIT UNLOCKED! KEEP MOVING!")
} else {
say("LOCKED! SEARCH THE HOUSE FOR A KEY.")
}
return
}
if (distance(px, pz, wardrobeX, wardrobeZ) < 1.25) {
hidden = !hidden
if (hidden) {
say("HIDDEN. PRESS E TO LEAVE THE WARDROBE.")
} else {
say("YOU LEFT THE WARDROBE.")
}
return
}
if (distance(px, pz, vaseX, vaseZ) < 1.25 &&
!vaseFalling && !vaseBroken) {
vaseFalling = true
vaseVelocity = 0.0
say("THE OBJECT IS FALLING!", 1.4)
return
}
if (distance(px, pz, keyX, keyZ) < 1.0 && !hasKey) {
hasKey = true
say("KEY COLLECTED! FIND THE EXIT.")
return
}
say("NOTHING TO INTERACT WITH HERE.")
}
// ----------------------------------------------------------
// ENEMY PATHFINDING
// ----------------------------------------------------------
def walkableCell(x: Int, z: Int): Boolean = {
if (x < 0 || z < 0 || x >= GW || z >= GH) return false
!blocked(
x * GRID + GRID / 2,
z * GRID + GRID / 2,
0.12
)
}
def nearestCell(wx: Double, wz: Double): Int = {
val ox = clamp((wx / GRID).toInt, 0, GW - 1).toInt
val oz = clamp((wz / GRID).toInt, 0, GH - 1).toInt
var result = -1
var radius = 0
while (result < 0 && radius < 10) {
var dz = -radius
while (dz <= radius && result < 0) {
var dx = -radius
while (dx <= radius && result < 0) {
if (math.abs(dx) == radius || math.abs(dz) == radius) {
val nx = ox + dx
val nz = oz + dz
if (walkableCell(nx, nz)) {
result = nz * GW + nx
}
}
dx += 1
}
dz += 1
}
radius += 1
}
result
}
def nextWaypoint(
sx: Double, sz: Double,
tx: Double, tz: Double
): (Double, Double) = {
val start = nearestCell(sx, sz)
val goal = nearestCell(tx, tz)
if (start < 0 || goal < 0) return (tx, tz)
if (start == goal) return (tx, tz)
val previous = Array.fill(GW * GH)(-1)
val queue = mutable.Queue[Int]()
previous(start) = start
queue.enqueue(start)
var iterations = 0
val dirs = Array(
(1, 0), (-1, 0),
(0, 1), (0, -1)
)
while (queue.nonEmpty &&
previous(goal) == -1 &&
iterations < GW * GH) {
val current = queue.dequeue()
val cx = current % GW
val cz = current / GW
for ((dx, dz) <- dirs) {
val nx = cx + dx
val nz = cz + dz
if (walkableCell(nx, nz)) {
val ni = nz * GW + nx
if (previous(ni) == -1) {
previous(ni) = current
queue.enqueue(ni)
}
}
}
iterations += 1
}
if (previous(goal) == -1) return (tx, tz)
var cur = goal
while (previous(cur) != start && cur != start &&
previous(cur) >= 0) {
cur = previous(cur)
}
(
(cur % GW) * GRID + GRID / 2,
(cur / GW) * GRID + GRID / 2
)
}
val patrol = Array(
(20.8, 3.0),
(21.8, 9.0),
(18.0, 14.0),
(10.0, 14.0),
(8.0, 8.5)
)
def resetDay(): Unit = {
day += 1
if (day > 5) {
gameOver = true
say("DAY 5 FINISHED. GAME OVER.", 20.0)
} else {
px = 3.2
pz = 4.2
yaw = math.Pi
pitch = 0.0
eyeRise = 0.0
hasKey = false
doorOpen = false
hidden = false
vaseY = 1.05
vaseVelocity = 0.0
vaseFalling = false
vaseBroken = false
noiseTimer = 0.0
enemyX = 21.0
enemyZ = 3.0
waypointX = enemyX
waypointZ = enemyZ
say("DAY " + day + " - YOU WOKE UP AGAIN!", 4.5)
}
}
def updateEnemy(dt: Double): Unit = {
if (hidden || gameOver || escaped) return
val playerDistance = distance(px, pz, enemyX, enemyZ)
var targetX = enemyX
var targetZ = enemyZ
if (noiseTimer > 0.0) {
targetX = noiseX
targetZ = noiseZ
} else if (playerDistance < 12.0) {
targetX = px
targetZ = pz
} else {
val p = patrol(patrolIndex)
targetX = p._1
targetZ = p._2
if (distance(enemyX, enemyZ, targetX, targetZ) < 0.7) {
patrolIndex = (patrolIndex + 1) % patrol.length
}
}
aiClock += dt
if (aiClock >= 0.65) {
aiClock = 0.0
val p = nextWaypoint(enemyX, enemyZ, targetX, targetZ)
waypointX = p._1
waypointZ = p._2
}
val dx = waypointX - enemyX
val dz = waypointZ - enemyZ
val len = math.sqrt(dx * dx + dz * dz)
if (len > 0.06) {
val step = math.min(len, 1.1 * dt)
val nx = enemyX + dx / len * step
val nz = enemyZ + dz / len * step
if (!blocked(nx, enemyZ, 0.18)) enemyX = nx
if (!blocked(enemyX, nz, 0.18)) enemyZ = nz
}
if (playerDistance < 0.65) {
resetDay()
}
}
// ----------------------------------------------------------
// GAME UPDATE
// ----------------------------------------------------------
def updateGame(): Unit = {
val dt = 0.033
if (!started || gameOver || escaped) return
if (messageTimer > 0.0) messageTimer -= dt
if (noiseTimer > 0.0) noiseTimer -= dt
// Rise from bed: camera height changes smoothly.
eyeRise = math.min(1.0, eyeRise + dt * 0.8)
if (vaseFalling && !vaseBroken) {
vaseVelocity += 9.8 * dt
vaseY -= vaseVelocity * dt
if (vaseY <= 0.12) {
vaseY = 0.12
vaseBroken = true
vaseFalling = false
noiseX = vaseX
noiseZ = vaseZ
noiseTimer = 12.0
say("CRASH! THE SOUND ATTRACTED THE ENEMY!", 4.0)
}
}
// Player movement.
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.2
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)
}
}
// Auto-pickup once the key is reached.
if (!hasKey && distance(px, pz, keyX, keyZ) < 0.55) {
hasKey = true
say("KEY COLLECTED! FIND THE EXIT.")
}
// Exit only after opening its locked door.
if (doorOpen &&
px > 19.4 && px < 21.6 &&
pz > 18.25) {
escaped = true
say("YOU ESCAPED THE OLD HOUSE!", 20.0)
}
updateEnemy(dt)
}
// ----------------------------------------------------------
// ENEMY MODEL
// ----------------------------------------------------------
def addEnemyModel(out: ArrayBuffer[Tri]): Unit = {
val ground = floorHeight(enemyX, enemyZ)
// Original, simple low-poly horror character.
dynamicBox(
out,
enemyX, ground + 1.05, enemyZ,
0.52, 1.15, 0.36,
new Color(65, 18, 22)
)
dynamicBox(
out,
enemyX, ground + 1.86, enemyZ,
0.43, 0.47, 0.43,
new Color(178, 156, 133)
)
dynamicBox(
out,
enemyX, ground + 2.08, enemyZ,
0.48, 0.17, 0.48,
new Color(25, 22, 20)
)
dynamicBox(
out,
enemyX - 0.37, ground + 1.15, enemyZ,
0.19, 0.88, 0.20,
new Color(95, 62, 48)
)
dynamicBox(
out,
enemyX + 0.37, ground + 1.15, enemyZ,
0.19, 0.88, 0.20,
new Color(95, 62, 48)
)
dynamicBox(
out,
enemyX - 0.16, ground + 0.32, enemyZ,
0.20, 0.65, 0.24,
new Color(35, 29, 28)
)
dynamicBox(
out,
enemyX + 0.16, ground + 0.32, enemyZ,
0.20, 0.65, 0.24,
new Color(35, 29, 28)
)
}
// ----------------------------------------------------------
// 3D CAMERA AND PERSPECTIVE RENDERING
// ----------------------------------------------------------
def cameraY: Double = {
floorHeight(px, pz) + 0.78 + 0.88 * eyeRise
}
def transform(v: V3): Cam = {
val dx = v.x - px
val dy = v.y - cameraY
val dz = v.z - pz
val d = dx * math.sin(yaw) + dz * math.cos(yaw)
val sx = dx * math.cos(yaw) - dz * math.sin(yaw)
val cp = math.cos(pitch)
val sp = math.sin(pitch)
Cam(
sx,
dy * cp - d * sp,
d * cp + dy * sp
)
}
def screenTriangle(
t: Tri,
w: Int,
h: Int
): Option[ScreenTri] = {
val a = transform(t.a)
val b = transform(t.b)
val c = transform(t.c)
if (a.z <= 0.12 || b.z <= 0.12 || c.z <= 0.12) {
return None
}
val focal = w * 0.84
def sx(p: Cam): Int =
clamp(w / 2.0 + p.x * focal / p.z, -5000, 5000).toInt
def sy(p: Cam): Int =
clamp(h / 2.0 - p.y * focal / p.z, -5000, 5000).toInt
val centerX = (t.a.x + t.b.x + t.c.x) / 3.0
val centerZ = (t.a.z + t.b.z + t.c.z) / 3.0
val dx = centerX - px
val dz = centerZ - pz
val dist = math.sqrt(dx * dx + dz * dz)
val direction =
if (dist < 0.001) 1.0
else (dx * math.sin(yaw) + dz * math.cos(yaw)) / dist
val lighting =
if (!flashlight) 0.13
else if (direction > 0.30) {
clamp(1.20 - dist * 0.035, 0.28, 1.15)
} else {
0.24
}
Some(
ScreenTri(
(a.z + b.z + c.z) / 3.0,
Array(sx(a), sx(b), sx(c)),
Array(sy(a), sy(b), sy(c)),
shade(t.color, lighting)
)
)
}
def draw3D(g: Graphics2D, w: Int, h: Int): Unit = {
val horizon = h / 2
// Ceiling darkness and atmospheric background.
g.setPaint(
new GradientPaint(
0f, 0f, new Color(8, 10, 14),
0f, horizon.toFloat, new Color(48, 43, 38)
)
)
g.fillRect(0, 0, w, horizon)
g.setPaint(
new GradientPaint(
0f, horizon.toFloat, new Color(47, 40, 32),
0f, h.toFloat, new Color(10, 10, 12)
)
)
g.fillRect(0, horizon, w, h - horizon)
val all = ArrayBuffer[Tri]()
all ++= worldTriangles
// Exit door.
if (!doorOpen) {
dynamicBox(
all,
doorX, 1.2, doorZ,
1.25, 2.4, 0.14,
new Color(65, 38, 26)
)
dynamicBox(
all,
doorX + 0.40, 1.15, doorZ - 0.09,
0.07, 0.12, 0.06,
new Color(205, 166, 50)
)
}
// Key model: gold shaft and ring.
if (!hasKey) {
dynamicBox(
all,
keyX, 0.24, keyZ,
0.42, 0.055, 0.055,
new Color(235, 184, 35)
)
dynamicBox(
all,
keyX - 0.23, 0.24, keyZ,
0.12, 0.06, 0.12,
new Color(245, 206, 66)
)
dynamicBox(
all,
keyX + 0.17, 0.24, keyZ,
0.06, 0.10, 0.08,
new Color(245, 206, 66)
)
}
// Vase before it breaks.
if (!vaseBroken) {
dynamicBox(
all,
vaseX, vaseY, vaseZ,
0.24, 0.34, 0.24,
new Color(115, 62, 48)
)
dynamicBox(
all,
vaseX, vaseY + 0.18, vaseZ,
0.15, 0.06, 0.15,
new Color(155, 99, 66)
)
} else {
dynamicBox(
all,
vaseX - 0.12, 0.06, vaseZ,
0.19, 0.05, 0.12,
new Color(115, 62, 48)
)
dynamicBox(
all,
vaseX + 0.12, 0.06, vaseZ + 0.05,
0.15, 0.04, 0.08,
new Color(155, 99, 66)
)
}
addEnemyModel(all)
// Painter's algorithm: far polygons first, near polygons last.
val projected = ArrayBuffer[ScreenTri]()
all.foreach { t =>
screenTriangle(t, w, h).foreach(projected += _)
}
val ordered = projected.sortWith(_.depth > _.depth)
ordered.foreach { t =>
g.setColor(t.color)
g.fillPolygon(
t.xs,
t.ys,
3
)
}
// Crosshair.
g.setColor(new Color(230, 230, 215, 180))
g.drawLine(w / 2 - 5, h / 2, w / 2 + 5, h / 2)
g.drawLine(w / 2, h / 2 - 5, w / 2, h / 2 + 5)
drawHUD(g, w, h)
if (showMap) drawMap(g, w)
}
// ----------------------------------------------------------
// HUD / DAY SYSTEM DISPLAY
// ----------------------------------------------------------
def drawHUD(g: Graphics2D, w: Int, h: Int): Unit = {
g.setColor(new Color(0, 0, 0, 175))
g.fillRect(0, 0, w, 65)
g.setFont(new Font("Arial", Font.BOLD, 17))
g.setColor(new Color(245, 222, 185))
g.drawString(
"DAY " + day +
" KEY: " + (if (hasKey) "YES" else "NO") +
" FLASHLIGHT: " + (if (flashlight) "ON" else "OFF"),
16, 25
)
g.setFont(new Font("Arial", Font.PLAIN, 14))
g.setColor(Color.WHITE)
g.drawString(
"WASD Move | Mouse Look | SHIFT Run | E Interact | F Light | M Map | R Restart",
16, 49
)
g.setColor(new Color(0, 0, 0, 185))
g.fillRect(0, h - 44, w, 44)
if (messageTimer > 0.0) {
g.setFont(new Font("Arial", Font.BOLD, 17))
g.setColor(new Color(255, 224, 165))
g.drawString(message, 16, h - 17)
}
if (distance(px, pz, enemyX, enemyZ) < 5.0 && !hidden) {
g.setFont(new Font("Arial", Font.BOLD, 16))
g.setColor(new Color(255, 65, 55))
g.drawString("SOMETHING IS NEAR...", w - 265, 89)
}
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(85, 255, 125)
else new Color(255, 75, 60))
val title =
if (escaped) "YOU ESCAPED!"
else "GAME OVER"
val fm = g.getFontMetrics
g.drawString(title, (w - fm.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 + 48
)
}
}
def drawMap(g: Graphics2D, w: Int): Unit = {
val ox = w - 200
val oy = 90
val sx = 7.0
val sz = 7.0
g.setColor(new Color(0, 0, 0, 190))
g.fillRect(ox - 6, oy - 6, 184, 144)
g.setColor(new Color(130, 120, 110))
collisionBoxes.foreach { b =>
g.fillRect(
ox + (b.x - b.sx / 2).toInt * sx.toInt,
oy + (b.z - b.sz / 2).toInt * sz.toInt,
math.max(1, (b.sx * sx).toInt),
math.max(1, (b.sz * sz).toInt)
)
}
g.setColor(Color.RED)
g.fillOval(
ox + (enemyX * sx).toInt - 3,
oy + (enemyZ * sz).toInt - 3,
7, 7
)
g.setColor(Color.CYAN)
g.fillOval(
ox + (px * sx).toInt - 3,
oy + (pz * sz).toInt - 3,
7, 7
)
}
// ----------------------------------------------------------
// START SCREEN
// ----------------------------------------------------------
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(55, 23, 20)
)
)
g.fillRect(0, 0, w, h)
g.setColor(new Color(25, 20, 18))
g.fillRect(100, 100, w - 200, h - 180)
g.setColor(new Color(61, 43, 32))
g.fillRect(170, 135, w - 340, h - 245)
// Bed silhouette.
g.setColor(new Color(31, 25, 24))
g.fillRect(190, h - 190, 280, 60)
g.setColor(new Color(95, 75, 64))
g.fillRect(210, h - 220, 240, 35)
// Door silhouette.
g.setColor(new Color(40, 24, 18))
g.fillRect(w - 320, 170, 105, 270)
g.setColor(new Color(198, 150, 62))
g.fillOval(w - 240, 305, 12, 12)
g.setFont(new Font("Arial", Font.BOLD, 48))
g.setColor(new Color(220, 210, 195))
val title = "OLD HOUSE 3D"
g.drawString(
title,
(w - g.getFontMetrics.stringWidth(title)) / 2,
85
)
g.setFont(new Font("Arial", Font.BOLD, 23))
g.setColor(new Color(255, 90, 70))
val subtitle = "DAY 1"
g.drawString(
subtitle,
(w - g.getFontMetrics.stringWidth(subtitle)) / 2,
h - 112
)
g.setFont(new Font("Arial", Font.BOLD, 21))
g.setColor(new Color(250, 220, 160))
val prompt = "PRESS ENTER TO WAKE UP"
g.drawString(
prompt,
(w - g.getFontMetrics.stringWidth(prompt)) / 2,
h - 62
)
g.setFont(new Font("Arial", Font.PLAIN, 16))
g.setColor(Color.LIGHT_GRAY)
val controls = "WASD - MOVE MOUSE - LOOK SHIFT - RUN"
g.drawString(
controls,
(w - g.getFontMetrics.stringWidth(controls)) / 2,
h - 25
)
}
// ----------------------------------------------------------
// MAIN WINDOW
// ----------------------------------------------------------
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 {
draw3D(g, getWidth, getHeight)
}
g.dispose()
}
}
def start(): Unit = {
buildHouse()
resetGame()
SwingUtilities.invokeLater(new Runnable {
def run(): Unit = {
frame = new JFrame("OLD HOUSE 3D - Scala Horror Game")
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_F) {
flashlight = !flashlight
say(
if (flashlight) "FLASHLIGHT ON"
else "FLASHLIGHT OFF"
)
}
if (k == KeyEvent.VK_M) showMap = !showMap
if (k == KeyEvent.VK_R) {
resetGame()
}
}
override def keyReleased(e: KeyEvent): Unit = {
keys.remove(e.getKeyCode)
}
})
panel.addMouseMotionListener(new MouseMotionAdapter {
override def mouseMoved(e: MouseEvent): Unit = {
if (started && !gameOver && !escaped) {
if (lastMouseX >= 0) {
val dx = e.getX - lastMouseX
val dy = e.getY - lastMouseY
yaw += dx * 0.006
pitch = clamp(pitch - dy * 0.004, -0.55, 0.55)
}
lastMouseX = e.getX
lastMouseY = e.getY
}
}
override def mouseDragged(e: MouseEvent): Unit = {
mouseMoved(e)
}
})
panel.addFocusListener(new java.awt.event.FocusAdapter {
override def focusLost(e: java.awt.event.FocusEvent): Unit = {
keys.clear()
lastMouseX = -1
lastMouseY = -1
}
})
frame.setContentPane(panel)
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE)
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()
}
})
}
}
OldHouse3D.start()