Code Sketch
3d granny yoi
Category: Programming
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 hasKey = 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 vaseX = 4.65
val vaseZ = 2.35
var vaseY = 0.9
var vaseVel = 0.0
var vaseFalling = false
var vaseBroken = false
var noiseX = vaseX
var noiseZ = vaseZ
var noiseTimer = 0.0
var enemyX = 20.5
var enemyZ = 3.2
var enemyUpper = false
var enemyTargetX = enemyX
var enemyTargetZ = enemyZ
var aiTimer = 0.0
var patrolIndex = 0
val patrol = Array((20.5, 3.2), (21.7, 8.0), (19.0, 12.8),
(16.0, 14.2), (8.5, 8.5))
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),(14.0,15.2)))
addWallX(12.0,0.25,17.75,Seq((3.0,4.2),(8.0,9.2),(14.6,15.8)))
addWallZ(6.0,0.25,23.75,Seq((3.5,4.8),(8.0,9.2),(14.3,17.3)))
addWallZ(12.0,0.25,23.75,Seq((2.0,3.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(6.0,14.6,1.2,"X",1.0,0.0,2.65,false,false,"Storage 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(2.6,12.0,1.2,"Z",1.0,0.0,2.65,false,false,"Lower room 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")
// 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(4.0,0.90,14.8,1.1,1.8,0.9,DARK_WOOD,true)
addBox(18.5,0.38,14.7,1.3,0.72,0.8,DARK_WOOD,true)
// 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 THE WARDROBE.")
return
}
if (dist(px,pz,wardrobeX,wardrobeZ) < 1.25 && !onUpperFloor) {
hidden = true
say("HIDDEN. PRESS E TO LEAVE THE WARDROBE.")
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.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; hasKey = false
doors.foreach(d => { d.angle = 0.0; d.target = 0.0 })
enemyX = 20.5; enemyZ = 3.2; 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
hasKey = 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 = 20.5; enemyZ = 3.2; 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 updateEnemy(dt: Double): Unit = {
if (hidden || gameOver || escaped) 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
}
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
wakeProgress = math.min(1.0,wakeProgress+dt*0.65)
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)
}
}
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))
}
// 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"),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/Open door | F Flashlight | M Map | R Restart",14,45)
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_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.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()
}
})
}
}
OldHouse3DImproved.start()