Code Sketch
yoii granny multiplayer
Category: Programming
import javax.swing._
import java.awt._
import java.awt.event._
import java.awt.image.BufferedImage
import java.awt.image.DataBufferInt
import java.net._
import java.io._
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, hiding, enemy AI,
* LAN two-player room discovery, remote avatar sync, shared quest items,
* garage / bunker / dock zones, car and boat escape objectives.
*
* 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
}
case class Pickup(name: String, x: Double, z: Double,
upper: Boolean, flag: String, color: Color)
case class RemotePlayer(x: Double, z: Double, yaw: Double,
upper: Boolean, role: String, active: Boolean)
// LAN discovery uses UDP; the actual shared player stream uses TCP.
object LANLink {
val TCP_PORT = 47888
val UDP_PORT = 47887
@volatile private var running = false
@volatile private var peerSocket: Socket = _
@volatile private var serverSocket: ServerSocket = _
@volatile private var writer: PrintWriter = _
@volatile var connected = false
@volatile var lastError = ""
private def attach(s: Socket): Unit = synchronized {
peerSocket = s
s.setTcpNoDelay(true)
writer = new PrintWriter(new OutputStreamWriter(s.getOutputStream, "UTF-8"), true)
connected = true
lastError = ""
val reader = new BufferedReader(new InputStreamReader(s.getInputStream, "UTF-8"))
val t = new Thread(new Runnable {
def run(): Unit = {
try {
var line = reader.readLine()
while (line != null && running) {
receiveNetworkState(line)
line = reader.readLine()
}
} catch { case _: Throwable => () }
connected = false
remoteActive = false
say("MULTIPLAYER CONNECTION CLOSED", 4.0)
}
}, "OldHouse-LAN-Reader")
t.setDaemon(true)
t.start()
}
def startHost(): Unit = {
running = true
try {
serverSocket = new ServerSocket(TCP_PORT)
val acceptThread = new Thread(new Runnable {
def run(): Unit = {
try {
val s = serverSocket.accept()
attach(s)
say("PLAYER 2 CONNECTED TO THE ROOM!", 5.0)
} catch { case e: Throwable => lastError = e.getMessage }
}
}, "OldHouse-LAN-Host")
acceptThread.setDaemon(true)
acceptThread.start()
val beaconThread = new Thread(new Runnable {
def run(): Unit = {
var ds: DatagramSocket = null
try {
ds = new DatagramSocket()
ds.setBroadcast(true)
val bytes = ("OLDHOUSE3D|" + TCP_PORT).getBytes("UTF-8")
val packet = new DatagramPacket(bytes, bytes.length,
InetAddress.getByName("255.255.255.255"), UDP_PORT)
while (running && !connected) {
try { ds.send(packet) } catch { case _: Throwable => () }
Thread.sleep(1200L)
}
} catch { case _: Throwable => () }
finally { if (ds != null) ds.close() }
}
}, "OldHouse-LAN-Discovery")
beaconThread.setDaemon(true)
beaconThread.start()
} catch {
case e: Throwable =>
lastError = Option(e.getMessage).getOrElse("Could not open LAN port")
say("LAN HOST ERROR: " + lastError, 8.0)
}
}
def discoverRoom(waitMs: Int): String = {
var ds: DatagramSocket = null
try {
ds = new DatagramSocket(UDP_PORT)
ds.setSoTimeout(650)
val endAt = System.currentTimeMillis() + waitMs
val buf = new Array[Byte](256)
while (System.currentTimeMillis() < endAt) {
val packet = new DatagramPacket(buf, buf.length)
try {
ds.receive(packet)
val msg = new String(packet.getData, 0, packet.getLength, "UTF-8")
if (msg.startsWith("OLDHOUSE3D|")) return packet.getAddress.getHostAddress
} catch { case _: java.net.SocketTimeoutException => () }
}
} catch { case e: Throwable => lastError = Option(e.getMessage).getOrElse("Discovery failed") }
finally { if (ds != null) ds.close() }
""
}
def join(host: String): Boolean = {
running = true
try {
val s = new Socket()
s.connect(new InetSocketAddress(host.trim, TCP_PORT), 5000)
attach(s)
true
} catch {
case e: Throwable =>
lastError = Option(e.getMessage).getOrElse("Could not join room")
connected = false
false
}
}
def send(line: String): Unit = {
val w = writer
if (connected && w != null) synchronized {
try { w.println(line); if (w.checkError()) connected = false }
catch { case _: Throwable => connected = false }
}
}
def close(): Unit = {
running = false
connected = false
remoteActive = false
try { if (peerSocket != null) peerSocket.close() } catch { case _: Throwable => () }
try { if (serverSocket != null) serverSocket.close() } catch { case _: Throwable => () }
peerSocket = null; serverSocket = null; writer = null
}
}
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
@volatile var gameOver = false
@volatile var escaped = false
var hidden = false
var hidePlace = ""
@volatile 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
@volatile var enemyStunnedTimer = 0.0
var secretUnlocked = false
var secretLootTaken = false
var flashlight = true
var showMap = false
@volatile var day = 1
var gameMode = "SINGLE"
var playerRole = "PLAYER"
var enemyType = "GRANNY"
@volatile var remoteRole = "PLAYER"
@volatile var remoteActive = false
@volatile var remoteX = 4.8
@volatile var remoteZ = 4.8
@volatile var remoteYaw = 0.0
@volatile var remoteUpper = false
@volatile var remoteHidden = false
@volatile var hasFuel = false
@volatile var hasBattery = false
@volatile var hasRope = false
@volatile var hasBoatKey = false
@volatile var hasWrench = false
@volatile var hasCode = false
@volatile var carEscaped = false
@volatile var boatEscaped = false
var netElapsed = 0.0
@volatile var selectedEscape = ""
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.
@volatile var enemyX = 9.4
@volatile var enemyZ = 8.3
@volatile 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))
// The six items are placed in different rooms / levels. Their positions
// stay fixed in this prototype; multiplayer shares collected objectives.
val pickups = Seq(
Pickup("FUEL CAN", 28.7, 4.9, false, "FUEL", new Color(218, 104, 35)),
Pickup("BATTERY", 29.2, 15.0, false, "BATTERY", new Color(80, 185, 215)),
Pickup("ROPE", 31.2, 18.4, false, "ROPE", new Color(191, 164, 112)),
Pickup("BOAT KEY", 28.7, 18.3, false, "BOATKEY", new Color(238, 194, 60)),
Pickup("WRENCH", 31.6, 14.6, false, "WRENCH", new Color(155, 165, 174)),
Pickup("GARAGE CODE", 21.0, 12.5, true, "CODE", new Color(177, 105, 225))
)
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
)
}
def configureGame(): Unit = {
val modes = Array[AnyRef]("Single Player", "Multiplayer - Host Room", "Multiplayer - Join Room")
val modeChoice = JOptionPane.showOptionDialog(null,
"Choose how you want to play OLD HOUSE 3D", "OLD HOUSE 3D - Game Mode",
JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, modes, modes(0))
if (modeChoice == 1) {
gameMode = "HOST"
playerRole = chooseRole()
LANLink.startHost()
JOptionPane.showMessageDialog(null,
"LAN room is starting on port " + LANLink.TCP_PORT + ".\nKeep this game open, then choose Join Room on the second computer.\nAllow Java through Windows Firewall when asked.\nIf auto-detect fails, run ipconfig on this PC and share its IPv4 Address.",
"HOST ROOM", JOptionPane.INFORMATION_MESSAGE)
} else if (modeChoice == 2) {
gameMode = "CLIENT"
playerRole = chooseRole()
val found = LANLink.discoverRoom(4500)
val host = if (found.nonEmpty) found else {
val typed = JOptionPane.showInputDialog(null,
"No room was auto-detected. Enter the HOST computer's local IPv4 address:",
"Join LAN Room", JOptionPane.QUESTION_MESSAGE)
Option(typed).getOrElse("").trim
}
if (host.nonEmpty && LANLink.join(host)) {
JOptionPane.showMessageDialog(null, "Connected to " + host + "! Both players can now enter the same house.",
"ROOM CONNECTED", JOptionPane.INFORMATION_MESSAGE)
} else {
gameMode = "SINGLE"
playerRole = "PLAYER"
JOptionPane.showMessageDialog(null,
"Could not connect to the room. Starting Single Player.\n" + LANLink.lastError,
"CONNECTION FAILED", JOptionPane.WARNING_MESSAGE)
LANLink.close()
}
} else {
gameMode = "SINGLE"
playerRole = "PLAYER"
val enemies = Array[AnyRef]("Granny AI", "Grandpa AI", "Robot AI", "Both - Granny + Grandpa")
val enemyChoice = JOptionPane.showOptionDialog(null,
"Choose the computer-controlled enemy", "ENEMY SELECT",
JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, enemies, enemies(0))
enemyType = if (enemyChoice == 1) "GRANDPA" else if (enemyChoice == 2) "ROBOT" else if (enemyChoice == 3) "BOTH" else "GRANNY"
}
}
def chooseRole(): String = {
val roles = Array[AnyRef]("Player / Survivor", "Granny", "Grandpa", "Granny + Grandpa (Both)")
val c = JOptionPane.showOptionDialog(null,
"Choose your character. The second computer chooses its own character.",
"CHARACTER SELECT", JOptionPane.DEFAULT_OPTION,
JOptionPane.QUESTION_MESSAGE, null, roles, roles(0))
if (c == 1) "GRANNY" else if (c == 2) "GRANDPA" else if (c == 3) "BOTH" else "PLAYER"
}
def pickupTaken(flag: String): Boolean = flag match {
case "FUEL" => hasFuel
case "BATTERY" => hasBattery
case "ROPE" => hasRope
case "BOATKEY" => hasBoatKey
case "WRENCH" => hasWrench
case "CODE" => hasCode
case _ => true
}
def takePickup(flag: String): Unit = flag match {
case "FUEL" => hasFuel = true
case "BATTERY" => hasBattery = true
case "ROPE" => hasRope = true
case "BOATKEY" => hasBoatKey = true
case "WRENCH" => hasWrench = true
case "CODE" => hasCode = true
case _ => ()
}
def carReady: Boolean = hasKey && hasFuel && hasBattery && hasCode
def boatReady: Boolean = hasBoatKey && hasFuel && hasBattery && hasRope && hasWrench
def receiveNetworkState(line: String): Unit = {
try {
val a = line.split("\\|", -1)
if (a.length >= 16) {
remoteX = a(0).toDouble
remoteZ = a(1).toDouble
remoteYaw = a(2).toDouble
remoteUpper = a(3) == "1"
remoteRole = a(4)
remoteActive = a(5) == "1"
if (a.length >= 23) remoteHidden = a(22) == "1"
if (a(13) == "1") { carEscaped = true; escaped = true; selectedEscape = "CAR" }
if (a(14) == "1") { boatEscaped = true; escaped = true; selectedEscape = "BOAT" }
if (a(15) == "1" && selectedEscape.isEmpty) escaped = true
if (gameMode == "CLIENT" && a.length >= 22) {
val hostDay = a(21).toInt
if (hostDay > day) {
day = hostDay
px = 4.8; pz = 4.15; yaw = -math.Pi/2.0; pitch = 0.0; onUpperFloor = false
hasKey = false; hasFuel = false; hasBattery = false; hasRope = false
hasBoatKey = false; hasWrench = false; hasCode = false
carEscaped = false; boatEscaped = false; escaped = false; selectedEscape = ""
secretUnlocked = false; secretLootTaken = false
doors.foreach(d => { d.angle = 0.0; d.target = 0.0 })
say("DAY " + day + " - YOUR TEAM RESTARTED THE ESCAPE!", 4.0)
}
enemyX = a(17).toDouble
enemyZ = a(18).toDouble
enemyUpper = a(19) == "1"
enemyStunnedTimer = a(20).toDouble
} else if (gameMode == "HOST" && a.length >= 22) {
val clientStun = a(20).toDouble
if (clientStun > enemyStunnedTimer) enemyStunnedTimer = clientStun
}
hasKey = hasKey || a(6) == "1"
hasFuel = hasFuel || a(7) == "1"
hasBattery = hasBattery || a(8) == "1"
hasRope = hasRope || a(9) == "1"
hasBoatKey = hasBoatKey || a(10) == "1"
hasWrench = hasWrench || a(11) == "1"
hasCode = hasCode || a(12) == "1"
if (a.length >= 17 && a(16).nonEmpty) {
val angles = a(16).split(",")
for (i <- 0 until math.min(doors.length, angles.length)) {
val v = angles(i).toDouble
if (v > doors(i).angle) { doors(i).angle = v; doors(i).target = v }
}
}
}
} catch { case _: Throwable => () }
}
def sendNetworkState(): Unit = {
if (!LANLink.connected) return
val upper = if (onUpperFloor) "1" else "0"
val active = if (started && !gameOver) "1" else "0"
def bit(v: Boolean) = if (v) "1" else "0"
val angles = doors.map(_.angle.toString).mkString(",")
val packet = Seq(px.toString, pz.toString, yaw.toString, upper, playerRole,
active, bit(hasKey), bit(hasFuel), bit(hasBattery), bit(hasRope),
bit(hasBoatKey), bit(hasWrench), bit(hasCode), bit(carEscaped),
bit(boatEscaped), bit(escaped), angles, enemyX.toString, enemyZ.toString,
bit(enemyUpper), enemyStunnedTimer.toString, day.toString, bit(hidden)).mkString("|")
LANLink.send(packet)
}
// -------------------------- 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)
// Side passage opens from the house into the outdoor escape yard.
addWallX(23.88,0.12,17.88,Seq((7.5,9.5)),0.0,2.8,WALL)
addDoor(23.88,8.5,2.0,"X",1.0,0.0,2.65,false,false,"Yard Exit")
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)
// ---------------- OUTDOOR ESCAPE YARD ----------------
addBox(29.7,-0.16,16.0,11.6,0.32,32.0,new Color(52,61,49),false)
addBox(35.45,1.35,16.0,0.22,2.7,32.0,new Color(52,48,39),true)
addBox(29.7,1.35,0.18,11.6,2.7,0.22,new Color(52,48,39),true)
addBox(29.7,1.35,31.82,11.6,2.7,0.22,new Color(52,48,39),true)
// Garage with a hinged entry gate and a small car model.
addWallX(27.25,2.2,8.0,Seq(),0.0,2.6,WALL_ALT)
addWallX(33.1,2.2,8.0,Seq(),0.0,2.6,WALL_ALT)
addWallZ(2.2,27.25,33.1,Seq(),0.0,2.6,WALL_ALT)
addWallZ(8.0,27.25,33.1,Seq((29.1,31.2)),0.0,2.6,WALL_ALT)
addDoor(30.15,8.0,2.1,"Z",1.0,0.0,2.5,false,false,"Garage Gate")
addBox(30.1,0.45,5.0,3.1,0.62,1.65,new Color(35,75,105),false)
addBox(30.1,0.95,5.0,1.65,0.55,1.35,new Color(45,91,118),false)
addBox(29.45,0.28,4.35,0.52,0.35,0.18,new Color(20,20,22),false)
addBox(30.75,0.28,4.35,0.52,0.35,0.18,new Color(20,20,22),false)
addBox(29.45,0.28,5.65,0.52,0.35,0.18,new Color(20,20,22),false)
addBox(30.75,0.28,5.65,0.52,0.35,0.18,new Color(20,20,22),false)
// Reinforced underground-looking bunker room, with a hinged entrance.
addWallX(26.5,13.0,20.0,Seq((15.7,17.1)),0.0,2.55,new Color(57,62,65))
addWallX(33.0,13.0,20.0,Seq(),0.0,2.55,new Color(57,62,65))
addWallZ(13.0,26.5,33.0,Seq(),0.0,2.55,new Color(57,62,65))
addWallZ(20.0,26.5,33.0,Seq(),0.0,2.55,new Color(57,62,65))
addDoor(26.5,16.4,1.4,"X",1.0,0.0,2.45,false,false,"Bunker Door")
addBox(29.65,2.25,13.25,1.2,0.45,0.45,new Color(75,82,88),false)
addBox(29.65,1.1,19.55,1.25,0.12,0.25,new Color(92,105,112),false)
// Wooden dock, water and a small escape boat.
addBox(29.8,-0.30,25.8,8.8,0.12,10.8,new Color(25,58,69),false)
addBox(30.0,0.02,25.2,4.8,0.20,4.4,new Color(105,73,48),false)
for (i <- 0 until 8) addBox(27.9+i*0.60,0.17,25.2,0.08,0.12,4.35,new Color(135,96,61),false)
addBox(30.0,0.30,28.0,2.5,0.48,4.5,new Color(72,54,43),false)
addBox(30.0,0.68,28.1,1.05,0.60,1.55,new Color(205,207,194),false)
addBox(30.0,1.00,28.1,0.92,0.06,1.30,new Color(48,76,91),false)
// 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 > 35.20 || z < 0.35 || z > 31.55) return true
if (z > 17.95 && x < 23.65) {
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
}
// Collect nearby quest objects. Both LAN players share collected flags.
val nearbyPickup = pickups.find(p => !pickupTaken(p.flag) && p.upper == onUpperFloor &&
dist(px,pz,p.x,p.z) < 1.35)
if (nearbyPickup.isDefined) {
val p = nearbyPickup.get
takePickup(p.flag)
say(p.name + " COLLECTED! CHECK THE ESCAPE TASKS.", 3.8)
return
}
if (!onUpperFloor && dist(px,pz,30.1,5.0) < 2.5) {
if (carReady) {
carEscaped = true; escaped = true; selectedEscape = "CAR"
say("CAR ESCAPE COMPLETE! YOU GOT OUT OF THE OLD HOUSE!", 20.0)
sendNetworkState()
} else {
say("CAR LOCKED. NEED: CAR KEY + FUEL + BATTERY + GARAGE CODE.", 4.5)
}
return
}
if (!onUpperFloor && dist(px,pz,30.0,28.0) < 2.8) {
if (boatReady) {
boatEscaped = true; escaped = true; selectedEscape = "BOAT"
say("BOAT ESCAPE COMPLETE! YOU ESCAPED BY SEA!", 20.0)
sendNetworkState()
} else {
say("BOAT NEEDS: BOAT KEY + FUEL + BATTERY + ROPE + WRENCH.", 4.5)
}
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 {
if (gameMode == "CLIENT") { px = 4.8; pz = 4.15; yaw = -math.Pi/2.0 }
else if (gameMode == "HOST") { px = 3.2; pz = 4.15; yaw = math.Pi/2.0 }
else { px = 3.2; pz = 4.15; yaw = math.Pi }
pitch = 0.0
onUpperFloor = false; hidden = false; hidePlace = ""; hasKey = false
hasFuel = false; hasBattery = false; hasRope = false; hasBoatKey = false
hasWrench = false; hasCode = false; carEscaped = false; boatEscaped = 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 = {
if (gameMode == "CLIENT") { px = 4.8; pz = 4.15; yaw = -math.Pi/2.0 }
else if (gameMode == "HOST") { px = 3.2; pz = 4.15; yaw = math.Pi/2.0 }
else { px = 3.2; pz = 4.15; yaw = math.Pi }
pitch = 0.0
if (playerRole == "GRANNY") enemyType = "GRANDPA"
else if (playerRole == "GRANDPA") enemyType = "GRANNY"
else if (playerRole == "BOTH") enemyType = "BOTH"
onUpperFloor = false; wakeProgress = 0.0
started = false; gameOver = false; escaped = false; hidden = false; hidePlace = ""
hasKey = false; hasFuel = false; hasBattery = false; hasRope = false
hasBoatKey = false; hasWrench = false; hasCode = false
carEscaped = false; boatEscaped = false; selectedEscape = ""
hasBomb = false; bombThrown = false; bombFuse = 0.0; bombBlastTimer = 0.0
enemyStunnedTimer = 0.0; secretUnlocked = false; secretLootTaken = false
flashlight = true; showMap = false; day = 1; netElapsed = 0.0
remoteActive = false; remoteRole = "PLAYER"
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 = 45; val h = 40
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 = {
val canTargetLocal = !hidden
val canTargetRemote = remoteActive && !remoteHidden
if ((!canTargetLocal && !canTargetRemote) || gameOver || escaped || enemyStunnedTimer > 0.0) return
val localDistance = if (canTargetLocal) dist(px,pz,enemyX,enemyZ) else Double.MaxValue
val remoteDistance = if (canTargetRemote) dist(remoteX,remoteZ,enemyX,enemyZ) else Double.MaxValue
val targetRemote = remoteDistance < localDistance
val dPlayer = math.min(localDistance,remoteDistance)
val targetPlayerX = if (targetRemote) remoteX else px
val targetPlayerZ = if (targetRemote) remoteZ else pz
val targetPlayerUpper = if (targetRemote) remoteUpper else onUpperFloor
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 host pursuer follows whichever visible teammate is closer.
if (targetPlayerUpper && !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 (!targetPlayerUpper && 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 = targetPlayerX; tz = targetPlayerZ; targetUpper = targetPlayerUpper
}
} 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 }
}
val caughtLocal = !hidden && enemyUpper == onUpperFloor && dist(px,pz,enemyX,enemyZ)<0.62
val caughtRemote = remoteActive && !remoteHidden && enemyUpper == remoteUpper &&
dist(remoteX,remoteZ,enemyX,enemyZ)<0.62
if (caughtLocal || caughtRemote) resetDay()
}
def updateGame(): Unit = {
val dt = 0.033
updateDoors(dt)
if (!started || gameOver || escaped) return
netElapsed += dt
if (netElapsed >= 0.066) { netElapsed = 0.0; sendNetworkState() }
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; selectedEscape = "MAIN EXIT"; say("YOU ESCAPED THE OLD HOUSE!",20.0)
sendNetworkState()
}
if (gameMode != "CLIENT") 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))
}
// Low-poly enemy: robot appears when both LAN users select Survivor.
val ey=if (enemyUpper) 3.0 else floorY(enemyX,enemyZ,false)
val robotMode = (gameMode != "SINGLE" && playerRole == "PLAYER" &&
remoteActive && remoteRole == "PLAYER") || (gameMode == "SINGLE" && enemyType == "ROBOT")
val enemyBody = if (robotMode) new Color(78,116,128) else new Color(65,17,22)
val enemyHead = if (robotMode) new Color(155,181,188) else new Color(174,151,127)
out ++= boxTriangles(Box(enemyX,ey+1.02,enemyZ,0.50,1.12,0.36,enemyBody,false))
out ++= boxTriangles(Box(enemyX,ey+1.78,enemyZ,0.42,0.43,0.42,enemyHead,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,enemyBody,false))
out ++= boxTriangles(Box(enemyX+0.32,ey+1.04,enemyZ,0.17,0.82,0.19,enemyBody,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))
// Quest pickups, rendered as bright small low-poly objects.
pickups.filter(p => !pickupTaken(p.flag)).foreach { p =>
val py = if (p.upper) 3.22 else 0.22
out ++= boxTriangles(Box(p.x,py,p.z,0.32,0.34,0.32,p.color,false))
out ++= boxTriangles(Box(p.x,py+0.20,p.z,0.18,0.08,0.18,shade(p.color,1.3),false))
}
// The remote LAN player is drawn in the same 3D world.
if (remoteActive) {
val avatarOffsets = if (remoteRole == "BOTH") Seq(-0.34, 0.34) else Seq(0.0)
avatarOffsets.foreach { offset =>
val ax = remoteX + offset
val ry = if (remoteUpper) 3.0 else floorY(ax,remoteZ,false)
val avatar = if (remoteRole == "GRANNY" || (remoteRole == "BOTH" && offset < 0.0)) new Color(145,76,145)
else if (remoteRole == "GRANDPA" || remoteRole == "BOTH") new Color(75,103,154)
else new Color(52,155,112)
out ++= boxTriangles(Box(ax,ry+1.02,remoteZ,0.48,1.08,0.34,avatar,false))
out ++= boxTriangles(Box(ax,ry+1.76,remoteZ,0.39,0.40,0.39,new Color(194,166,139),false))
out ++= boxTriangles(Box(ax,ry+0.31,remoteZ-0.10,0.16,0.58,0.18,new Color(35,31,30),false))
out ++= boxTriangles(Box(ax,ry+0.31,remoteZ+0.10,0.16,0.58,0.18,new Color(35,31,30),false))
}
}
// BOTH mode shows a second pursuer alongside the first AI.
if (enemyType == "BOTH" || playerRole == "BOTH" || remoteRole == "BOTH") {
val ex2 = enemyX + 0.72
out ++= boxTriangles(Box(ex2,ey+1.02,enemyZ+0.18,0.44,1.04,0.34,new Color(107,48,73),false))
out ++= boxTriangles(Box(ex2,ey+1.76,enemyZ+0.18,0.38,0.39,0.38,new Color(183,158,139),false))
out ++= boxTriangles(Box(ex2,ey+0.30,enemyZ+0.18,0.16,0.58,0.18,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 = {
// Scaled to include the house, outdoor yard, garage, bunker and dock.
val ox=w-130; val oy=145; val scale=3.1
g.setColor(new Color(0,0,0,190)); g.fillRect(ox-6,oy-6,130,110)
g.setColor(new Color(125,112,96))
solids.foreach { b =>
if (b.y < 2.0) g.fillRect(ox+((b.x-b.sx/2)*scale).toInt,
oy+((b.z-b.sz/2)*scale).toInt,math.max(1,(b.sx*scale).toInt),math.max(1,(b.sz*scale).toInt))
}
pickups.filter(p => !pickupTaken(p.flag)).foreach { p =>
g.setColor(p.color)
g.fillOval(ox+(p.x*scale).toInt-2,oy+(p.z*scale).toInt-2,5,5)
}
g.setColor(Color.RED); g.fillOval(ox+(enemyX*scale).toInt-3,oy+(enemyZ*scale).toInt-3,7,7)
if (remoteActive) {
g.setColor(new Color(90,220,120))
g.fillOval(ox+(remoteX*scale).toInt-3,oy+(remoteZ*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,190)); g.fillRect(0,0,w,125)
g.setFont(new Font("Arial",Font.BOLD,16)); g.setColor(new Color(245,222,184))
g.drawString("DAY "+day+" ROLE: "+playerRole+" 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 | F Light | M Map | R Restart",14,45)
g.setColor(new Color(245,222,184))
val connection = if (gameMode == "SINGLE") "SINGLE PLAYER" else if (LANLink.connected) "2 PLAYERS CONNECTED" else "WAITING FOR PLAYER 2"
g.drawString("TASKS KEY:"+(if(hasKey) "OK" else "--")+
" FUEL:"+(if(hasFuel) "OK" else "--")+" BATTERY:"+(if(hasBattery) "OK" else "--")+
" ROPE:"+(if(hasRope) "OK" else "--")+" BOAT KEY:"+(if(hasBoatKey) "OK" else "--")+
" WRENCH:"+(if(hasWrench) "OK" else "--")+" CODE:"+(if(hasCode) "OK" else "--"),14,76)
g.drawString("CAR "+(if(carReady) "READY" else "LOCKED")+" | BOAT "+(if(boatReady) "READY" else "LOCKED")+
" | "+connection+(if(remoteActive) " | PARTNER: "+remoteRole else ""),14,96)
if (hidden) g.drawString("HIDING: "+hidePlace,14,116)
if (enemyStunnedTimer>0.0) g.drawString("PURSUER STUNNED",250,116)
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))
val warning = if ((gameMode != "SINGLE" && playerRole == "PLAYER" && remoteActive && remoteRole == "PLAYER") || enemyType == "ROBOT") "ROBOT IS NEAR..." else "SOMETHING IS NEAR..."
g.drawString(warning,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)
val route = if (selectedEscape.nonEmpty) "ESCAPE ROUTE: " + selectedEscape else ""
g.drawString(route,w/2-120,h/2+16)
g.drawString("Press R to restart",w/2-95,h/2+54)
}
}
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 - ULTIMATE"; 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 START YOUR ESCAPE"; g.drawString(p,(w-g.getFontMetrics.stringWidth(p))/2,h-62)
g.setFont(new Font("Arial",Font.PLAIN,15)); g.setColor(new Color(220,210,195))
val hint="Find the key, fuel, battery, rope and tools. Escape by car or boat."
g.drawString(hint,(w-g.getFontMetrics.stringWidth(hint))/2,h-28)
}
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 = {
configureGame()
buildHouse(); resetGame()
SwingUtilities.invokeLater(new Runnable {
def run(): Unit = {
frame=new JFrame("OLD HOUSE 3D - ULTIMATE MULTIPLAYER ESCAPE")
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 WindowAdapter {
override def windowClosing(e: WindowEvent): Unit = {
try { if (gameTimer != null) gameTimer.stop() } catch { case _: Throwable => () }
LANLink.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()