Code Sketch
granny
Category: Programming
import java.awt._
import java.awt.event._
import java.awt.geom.Point2D
import javax.swing._
import scala.collection.mutable.ArrayBuffer
/*
* ================================================================
* OLD HOUSE ESCAPE 3D
* Kojo + Scala + Java Swing/AWT
* ================================================================
*
* Controls:
* ENTER Start / Restart
* W A S D Move
* SHIFT Sprint
* C Crouch
* Mouse Look
* Arrow Keys Look fallback
* E Interact / Collect / Hide / Exit
* F Flashlight
* ESC Quit
*
* No external images / models / assets required.
* ================================================================
*/
object OldHouseEscape3D {
// ================================================================
// GAME PANEL
// ================================================================
class Game
extends JPanel
with KeyListener
with MouseListener
with MouseMotionListener {
// ==============================================================
// WORLD
// ==============================================================
val MAP_W = 31
val MAP_H = 25
val world = Array.fill(MAP_H, MAP_W)(false)
// Outer walls
for (z <- 0 until MAP_H) {
for (x <- 0 until MAP_W) {
if (
x == 0 ||
z == 0 ||
x == MAP_W - 1 ||
z == MAP_H - 1
) {
world(z)(x) = true
}
}
}
def wall(x: Int, z: Int): Unit = {
if (
x >= 0 &&
x < MAP_W &&
z >= 0 &&
z < MAP_H
) {
world(z)(x) = true
}
}
// Vertical house sections
for (z <- 2 to 22 if z != 5 && z != 11 && z != 18) {
wall(8, z)
}
for (
z <- 2 to 22
if z != 4 && z != 9 && z != 16 && z != 21
) {
wall(15, z)
}
for (
z <- 2 to 22
if z != 6 && z != 12 && z != 20
) {
wall(23, z)
}
// Horizontal house sections
for (
x <- 2 to 28
if x != 5 && x != 12 && x != 19 && x != 26
) {
wall(x, 7)
}
for (
x <- 2 to 28
if x != 4 && x != 11 && x != 18 && x != 26
) {
wall(x, 14)
}
for (
x <- 2 to 28
if x != 6 && x != 16 && x != 21 && x != 27
) {
wall(x, 19)
}
// South exit opening
world(24)(14) = false
// ==============================================================
// PLAYER / CAMERA
// ==============================================================
var px = 3.5
var py = 1.45
var pz = 3.5
var yaw = 0.0
var pitch = 0.0
val FOCAL = 690.0
val NEAR = 0.06
var started = false
var escaped = false
var gameOver = false
var night = 1
val MAX_NIGHTS = 5
var nightTime = 150.0
var crouched = false
var hidden = false
var flashlight = true
var sprintEnergy = 100.0
// ==============================================================
// MESSAGE
// ==============================================================
var msg = "PRESS ENTER TO START"
var msgTime = 999.0
def message(s: String, t: Double): Unit = {
msg = s
msgTime = t
}
// ==============================================================
// ITEMS
// ==============================================================
case class Item(
name: String,
var x: Double,
var z: Double,
var collected: Boolean,
kind: Int
)
val items = Array(
Item("RUSTED KEY", 5.5, 4.5, false, 0),
Item("FUSE", 18.5, 9.5, false, 1),
Item("CUTTER", 27.5, 16.5, false, 2)
)
// ==============================================================
// HIDING PLACES
// ==============================================================
case class Hide(
name: String,
x: Double,
z: Double
)
val hidingPlaces = Array(
Hide("WARDROBE", 4.5, 10.5),
Hide("CABINET", 19.5, 4.5),
Hide("STORAGE", 26.5, 18.5)
)
// ==============================================================
// ENEMY AI
// ==============================================================
var enemyX = 27.5
var enemyZ = 3.5
var enemyDir = math.Pi
var enemyMode = "PATROL"
var targetX = enemyX
var targetZ = enemyZ
var lastSeenX = enemyX
var lastSeenZ = enemyZ
var searchTime = 0.0
var attackCooldown = 0.0
val patrol = Array(
(27.5, 3.5),
(27.5, 10.5),
(27.5, 17.5),
(21.5, 17.5),
(17.5, 17.5),
(17.5, 12.5),
(11.5, 12.5),
(11.5, 4.5),
(5.5, 4.5),
(5.5, 16.5),
(12.5, 21.5),
(21.5, 21.5)
)
var patrolIndex = 0
// ==============================================================
// INPUT
// ==============================================================
val held =
scala.collection.mutable.Set[Int]()
var interactPressed = false
var lastMouseX = -1
var lastMouseY = -1
var mouseInside = false
// ==============================================================
// NOISE
// ==============================================================
var noiseX = px
var noiseZ = pz
var noisePower = 0.0
var stepNoiseTimer = 0.0
// ==============================================================
// CLOCK
// ==============================================================
var lastTime = System.nanoTime()
val timer = new javax.swing.Timer(
16,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
updateGame()
repaint()
}
}
)
// ==============================================================
// PANEL SETUP
// ==============================================================
setPreferredSize(
new Dimension(1280, 760)
)
setFocusable(true)
setFocusTraversalKeysEnabled(false)
setDoubleBuffered(true)
addKeyListener(this)
addMouseListener(this)
addMouseMotionListener(this)
setBackground(
new Color(3, 4, 7)
)
override def addNotify(): Unit = {
super.addNotify()
requestFocusInWindow()
lastTime = System.nanoTime()
if (!timer.isRunning) {
timer.start()
}
}
// ==============================================================
// MATH
// ==============================================================
def distance(
ax: Double,
az: Double,
bx: Double,
bz: Double
): Double = {
math.hypot(
ax - bx,
az - bz
)
}
def isWall(
x: Int,
z: Int
): Boolean = {
if (
x < 0 ||
x >= MAP_W ||
z < 0 ||
z >= MAP_H
) {
true
} else {
world(z)(x)
}
}
def canWalk(
x: Double,
z: Double
): Boolean = {
val r = 0.22
val checks = Array(
(x - r, z - r),
(x + r, z - r),
(x - r, z + r),
(x + r, z + r)
)
checks.forall {
case (xx, zz) =>
!isWall(
math.floor(xx).toInt,
math.floor(zz).toInt
)
}
}
def normalizeAngle(
a: Double
): Double = {
var v = a
while (v > math.Pi) {
v -= math.Pi * 2.0
}
while (v < -math.Pi) {
v += math.Pi * 2.0
}
v
}
// ==============================================================
// LINE OF SIGHT
// ==============================================================
def clearSight(
ax: Double,
az: Double,
bx: Double,
bz: Double
): Boolean = {
val d =
distance(
ax,
az,
bx,
bz
)
val steps =
math.max(
1,
(d / 0.11).toInt
)
var i = 1
while (i < steps) {
val t =
i.toDouble /
steps.toDouble
val x =
ax + (bx - ax) * t
val z =
az + (bz - az) * t
if (
isWall(
math.floor(x).toInt,
math.floor(z).toInt
)
) {
return false
}
i += 1
}
true
}
def enemyCanSeePlayer: Boolean = {
if (
hidden ||
!started ||
gameOver ||
escaped
) {
false
} else {
val dx = px - enemyX
val dz = pz - enemyZ
val d =
math.hypot(dx, dz)
val maxDistance =
if (crouched) 5.2
else 8.8
val desired =
math.atan2(dx, dz)
val difference =
math.abs(
normalizeAngle(
desired - enemyDir
)
)
d < maxDistance &&
difference < 1.20 &&
clearSight(
enemyX,
enemyZ,
px,
pz
)
}
}
// ==============================================================
// PATH FINDING
// ==============================================================
def nextStep(
sx: Int,
sz: Int,
tx: Int,
tz: Int
): (Int, Int) = {
if (
sx == tx &&
sz == tz
) {
return (sx, sz)
}
if (isWall(tx, tz)) {
return (-1, -1)
}
val visited =
Array.fill(
MAP_H,
MAP_W
)(false)
val prevX =
Array.fill(
MAP_H,
MAP_W
)(-1)
val prevZ =
Array.fill(
MAP_H,
MAP_W
)(-1)
val q =
new java.util.ArrayDeque[(Int, Int)]()
q.add((sx, sz))
visited(sz)(sx) = true
val dirs = Array(
(1, 0),
(-1, 0),
(0, 1),
(0, -1)
)
var found = false
while (
!q.isEmpty &&
!found
) {
val p = q.removeFirst()
var j = 0
while (
j < dirs.length
) {
val nx =
p._1 + dirs(j)._1
val nz =
p._2 + dirs(j)._2
if (
nx >= 0 &&
nx < MAP_W &&
nz >= 0 &&
nz < MAP_H &&
!isWall(nx, nz) &&
!visited(nz)(nx)
) {
visited(nz)(nx) = true
prevX(nz)(nx) = p._1
prevZ(nz)(nx) = p._2
q.add((nx, nz))
if (
nx == tx &&
nz == tz
) {
found = true
}
}
j += 1
}
}
if (!visited(tz)(tx)) {
return (-1, -1)
}
var cx = tx
var cz = tz
while (
!(
prevX(cz)(cx) == sx &&
prevZ(cz)(cx) == sz
)
) {
val ax = prevX(cz)(cx)
val az = prevZ(cz)(cx)
if (
ax < 0 ||
az < 0
) {
return (-1, -1)
}
cx = ax
cz = az
}
(cx, cz)
}
// ==============================================================
// ENEMY MOVEMENT
// ==============================================================
def moveEnemyTo(
tx: Double,
tz: Double,
speed: Double,
dt: Double
): Unit = {
val sx =
math.floor(enemyX).toInt
val sz =
math.floor(enemyZ).toInt
val txc =
math.floor(tx).toInt
val tzc =
math.floor(tz).toInt
val step =
nextStep(
sx,
sz,
txc,
tzc
)
val gx =
if (step._1 < 0)
tx
else
step._1 + 0.5
val gz =
if (step._2 < 0)
tz
else
step._2 + 0.5
val dx =
gx - enemyX
val dz =
gz - enemyZ
val d =
math.hypot(dx, dz)
if (d > 0.03) {
val amount =
math.min(
speed * dt,
d
)
val nx =
dx / d
val nz =
dz / d
val ex =
enemyX + nx * amount
val ez =
enemyZ + nz * amount
if (
canWalk(
ex,
enemyZ
)
) {
enemyX = ex
}
if (
canWalk(
enemyX,
ez
)
) {
enemyZ = ez
}
enemyDir =
math.atan2(dx, dz)
}
}
// ==============================================================
// ENEMY AI
// ==============================================================
def updateEnemy(
dt: Double
): Unit = {
if (attackCooldown > 0) {
attackCooldown -= dt
}
if (enemyCanSeePlayer) {
enemyMode = "CHASE"
lastSeenX = px
lastSeenZ = pz
searchTime = 5.5
} else if (
noisePower > 0 &&
!hidden &&
distance(
enemyX,
enemyZ,
noiseX,
noiseZ
) < noisePower
) {
enemyMode = "INVESTIGATE"
targetX = noiseX
targetZ = noiseZ
searchTime = 4.5
}
enemyMode match {
case "PATROL" =>
val p =
patrol(patrolIndex)
moveEnemyTo(
p._1,
p._2,
1.20 +
(night - 1) * 0.11,
dt
)
if (
distance(
enemyX,
enemyZ,
p._1,
p._2
) < 0.50
) {
patrolIndex =
(patrolIndex + 1) %
patrol.length
}
case "INVESTIGATE" =>
moveEnemyTo(
targetX,
targetZ,
1.38 +
(night - 1) * 0.12,
dt
)
if (
distance(
enemyX,
enemyZ,
targetX,
targetZ
) < 0.55
) {
enemyMode = "SEARCH"
lastSeenX = targetX
lastSeenZ = targetZ
searchTime = 3.8
}
case "CHASE" =>
lastSeenX = px
lastSeenZ = pz
moveEnemyTo(
px,
pz,
2.05 +
(night - 1) * 0.17,
dt
)
searchTime -= dt
if (
searchTime <= 0 &&
!enemyCanSeePlayer
) {
enemyMode = "SEARCH"
searchTime = 3.2
}
case "SEARCH" =>
moveEnemyTo(
lastSeenX,
lastSeenZ,
1.46 +
(night - 1) * 0.10,
dt
)
searchTime -= dt
if (
searchTime <= 0 &&
distance(
enemyX,
enemyZ,
lastSeenX,
lastSeenZ
) < 0.85
) {
enemyMode = "PATROL"
}
case _ =>
enemyMode = "PATROL"
}
if (
!hidden &&
attackCooldown <= 0 &&
distance(
px,
pz,
enemyX,
enemyZ
) < 0.66
) {
caught()
attackCooldown = 1.5
}
}
// ==============================================================
// RESET NIGHT
// ==============================================================
def resetNight(): Unit = {
px = 3.5
py = 1.45
pz = 3.5
yaw = 0.0
pitch = 0.0
enemyX = 27.5
enemyZ = 3.5
enemyDir = math.Pi
enemyMode = "PATROL"
patrolIndex = 0
hidden = false
crouched = false
flashlight = true
nightTime = 150.0
sprintEnergy = 100.0
noisePower = 0.0
stepNoiseTimer = 0.0
interactPressed = false
val layouts = Array(
Array(
(5.5, 4.5),
(18.5, 9.5),
(27.5, 16.5)
),
Array(
(4.5, 5.5),
(19.5, 12.5),
(26.5, 16.5)
),
Array(
(6.5, 16.5),
(17.5, 4.5),
(27.5, 11.5)
),
Array(
(5.5, 11.5),
(17.5, 16.5),
(27.5, 4.5)
),
Array(
(4.5, 4.5),
(19.5, 9.5),
(27.5, 17.5)
)
)
val set =
layouts(
math.min(
night - 1,
layouts.length - 1
)
)
var i = 0
while (i < items.length) {
items(i).x =
set(i)._1
items(i).z =
set(i)._2
items(i).collected =
false
i += 1
}
}
// ==============================================================
// CAUGHT
// ==============================================================
def caught(): Unit = {
night += 1
if (night > MAX_NIGHTS) {
gameOver = true
message(
"THE HOUSE WON - PRESS ENTER TO RESTART",
999
)
} else {
resetNight()
message(
"THE CARETAKER CAUGHT YOU - NIGHT " +
night,
3.0
)
}
}
// ==============================================================
// INTERACT
// ==============================================================
def interactNow(): Unit = {
if (
!started ||
escaped ||
gameOver
) {
return
}
// Leave hiding place
if (hidden) {
hidden = false
message(
"YOU LEAVE THE HIDING PLACE",
1.5
)
return
}
// Find nearest item
var nearest: Item = null
var best = 99.0
var i = 0
while (i < items.length) {
val it = items(i)
if (!it.collected) {
val d =
distance(
px,
pz,
it.x,
it.z
)
if (
d < 1.20 &&
d < best
) {
nearest = it
best = d
}
}
i += 1
}
if (nearest != null) {
nearest.collected = true
noiseX = px
noiseZ = pz
noisePower = 7.5
message(
"FOUND: " +
nearest.name,
2.4
)
return
}
// Hiding places
for (h <- hidingPlaces) {
if (
distance(
px,
pz,
h.x,
h.z
) < 1.10
) {
hidden = true
message(
"HIDDEN IN " +
h.name +
" - PRESS E TO LEAVE",
2.0
)
return
}
}
// Exit
if (
px > 12.8 &&
px < 16.0 &&
pz > 23.0
) {
if (
items.forall(
_.collected
)
) {
escaped = true
message(
"YOU ESCAPED THE OLD HOUSE!",
999
)
} else {
message(
"EXIT LOCKED - FIND ALL 3 OBJECTS",
2.4
)
}
}
}
// ==============================================================
// GAME UPDATE
// ==============================================================
def updateGame(): Unit = {
val now =
System.nanoTime()
var dt =
(now - lastTime) /
1000000000.0
lastTime = now
if (
dt <= 0 ||
dt > 0.05
) {
dt = 0.016
}
if (msgTime > 0) {
msgTime -= dt
}
noisePower =
math.max(
0.0,
noisePower - dt * 4.5
)
// ------------------------------------------------------------
// Start screen
// ------------------------------------------------------------
if (!started) {
if (
held.contains(
KeyEvent.VK_ENTER
)
) {
started = true
escaped = false
gameOver = false
night = 1
resetNight()
message(
"NIGHT 1 - FIND THE OBJECTS AND ESCAPE",
3.0
)
held.remove(
KeyEvent.VK_ENTER
)
}
return
}
// ------------------------------------------------------------
// End screen
// ------------------------------------------------------------
if (
gameOver ||
escaped
) {
if (
held.contains(
KeyEvent.VK_ENTER
)
) {
started = false
escaped = false
gameOver = false
night = 1
held.remove(
KeyEvent.VK_ENTER
)
message(
"PRESS ENTER TO START",
999
)
}
return
}
// ------------------------------------------------------------
// Crouch
// ------------------------------------------------------------
crouched =
held.contains(
KeyEvent.VK_C
)
// ------------------------------------------------------------
// Arrow-key camera
// ------------------------------------------------------------
if (
held.contains(
KeyEvent.VK_LEFT
)
) {
yaw -= dt * 1.8
}
if (
held.contains(
KeyEvent.VK_RIGHT
)
) {
yaw += dt * 1.8
}
if (
held.contains(
KeyEvent.VK_UP
)
) {
pitch =
math.max(
-0.45,
pitch - dt
)
}
if (
held.contains(
KeyEvent.VK_DOWN
)
) {
pitch =
math.min(
0.45,
pitch + dt
)
}
// ------------------------------------------------------------
// Flashlight
// ------------------------------------------------------------
if (
held.contains(
KeyEvent.VK_F
)
) {
flashlight =
!flashlight
held.remove(
KeyEvent.VK_F
)
message(
if (flashlight)
"FLASHLIGHT ON"
else
"FLASHLIGHT OFF",
1.2
)
}
// ------------------------------------------------------------
// Interaction
// ------------------------------------------------------------
if (interactPressed) {
interactNow()
interactPressed = false
}
// ------------------------------------------------------------
// Movement
// ------------------------------------------------------------
var mx = 0.0
var mz = 0.0
if (
held.contains(
KeyEvent.VK_W
)
) {
mx += math.sin(yaw)
mz += math.cos(yaw)
}
if (
held.contains(
KeyEvent.VK_S
)
) {
mx -= math.sin(yaw)
mz -= math.cos(yaw)
}
if (
held.contains(
KeyEvent.VK_A
)
) {
mx -= math.cos(yaw)
mz += math.sin(yaw)
}
if (
held.contains(
KeyEvent.VK_D
)
) {
mx += math.cos(yaw)
mz -= math.sin(yaw)
}
val rawLen =
math.hypot(mx, mz)
val moving =
rawLen > 0.001
val sprinting =
moving &&
held.contains(
KeyEvent.VK_SHIFT
) &&
!crouched &&
sprintEnergy > 0.5
val speed =
if (crouched)
1.05
else if (sprinting)
4.0
else
2.45
if (
moving &&
!hidden
) {
val nx =
mx /
rawLen *
speed *
dt
val nz =
mz /
rawLen *
speed *
dt
movePlayer(
nx,
nz
)
if (sprinting) {
sprintEnergy =
math.max(
0.0,
sprintEnergy -
28.0 * dt
)
} else {
sprintEnergy =
math.min(
100.0,
sprintEnergy +
11.0 * dt
)
}
// Sound
if (!crouched) {
stepNoiseTimer -= dt
if (
stepNoiseTimer <= 0
) {
noiseX = px
noiseZ = pz
noisePower =
if (sprinting)
12.0
else
7.0
stepNoiseTimer =
if (sprinting)
0.30
else
0.66
}
}
} else {
sprintEnergy =
math.min(
100.0,
sprintEnergy +
14.0 * dt
)
}
// Enemy
updateEnemy(dt)
// Night timer
nightTime -= dt
if (nightTime <= 0) {
night += 1
if (night > MAX_NIGHTS) {
gameOver = true
message(
"FIVE NIGHTS PASSED - PRESS ENTER",
999
)
} else {
resetNight()
message(
"A NEW NIGHT BEGINS...",
2.5
)
}
}
}
// ==============================================================
// PLAYER MOVEMENT
// ==============================================================
def movePlayer(
dx: Double,
dz: Double
): Unit = {
if (hidden) {
return
}
val nx = px + dx
val nz = pz + dz
if (
canWalk(
nx,
pz
)
) {
px = nx
}
if (
canWalk(
px,
nz
)
) {
pz = nz
}
}
// ==============================================================
// 3D RENDER STRUCTURES
// ==============================================================
case class V(
x: Double,
y: Double,
z: Double
)
case class ScreenPoint(
x: Int,
y: Int,
depth: Double
)
case class Poly(
pts: Array[V],
shade: Double,
col: Color
)
// ==============================================================
// CAMERA
// ==============================================================
def camera(
v: V
): Option[V] = {
val dx = v.x - px
val dy = v.y - py
val dz = v.z - pz
val c =
math.cos(yaw)
val s =
math.sin(yaw)
val cx =
c * dx - s * dz
val cz =
s * dx + c * dz
if (cz <= NEAR) {
None
} else {
Some(
V(
cx,
dy,
cz
)
)
}
}
def project(
v: V
): Option[ScreenPoint] = {
camera(v) match {
case None =>
None
case Some(c) =>
val sx =
getWidth / 2.0 +
c.x *
FOCAL /
c.z
val sy =
getHeight / 2.0 -
c.y *
FOCAL /
c.z +
pitch *
250.0
Some(
ScreenPoint(
sx.toInt,
sy.toInt,
c.z
)
)
}
}
// ==============================================================
// CUBE
// ==============================================================
def cube(
polys: ArrayBuffer[Poly],
cx: Double,
cy: Double,
cz: Double,
sx: Double,
sy: Double,
sz: Double,
color: Color
): Unit = {
val x0 =
cx - sx / 2
val x1 =
cx + sx / 2
val y0 =
cy - sy / 2
val y1 =
cy + sy / 2
val z0 =
cz - sz / 2
val z1 =
cz + sz / 2
val a =
V(x0, y0, z0)
val b =
V(x1, y0, z0)
val c =
V(x1, y1, z0)
val d =
V(x0, y1, z0)
val e =
V(x0, y0, z1)
val f =
V(x1, y0, z1)
val gg =
V(x1, y1, z1)
val h =
V(x0, y1, z1)
polys +=
Poly(
Array(a, b, c, d),
0.66,
color
)
polys +=
Poly(
Array(f, e, h, gg),
0.58,
color
)
polys +=
Poly(
Array(e, a, d, h),
0.62,
color
)
polys +=
Poly(
Array(b, f, gg, c),
0.82,
color
)
polys +=
Poly(
Array(d, c, gg, h),
1.00,
color
)
polys +=
Poly(
Array(e, f, b, a),
0.42,
color
)
}
// ==============================================================
// FLOOR
// ==============================================================
def renderFloor(
polys: ArrayBuffer[Poly]
): Unit = {
for (
z <- 1 until MAP_H - 1;
x <- 1 until MAP_W - 1;
if !world(z)(x)
) {
val d =
distance(
px,
pz,
x + 0.5,
z + 0.5
)
if (d < 22) {
val c =
if (
(x + z) % 2 == 0
)
new Color(
58,
57,
58
)
else
new Color(
47,
47,
49
)
polys +=
Poly(
Array(
V(x, 0, z),
V(x + 1, 0, z),
V(
x + 1,
0,
z + 1
),
V(
x,
0,
z + 1
)
),
0.78,
c
)
}
}
}
// ==============================================================
// CEILING
// ==============================================================
def renderCeiling(
polys: ArrayBuffer[Poly]
): Unit = {
for (
z <- 1 until MAP_H - 1;
x <- 1 until MAP_W - 1;
if !world(z)(x)
) {
val d =
distance(
px,
pz,
x + 0.5,
z + 0.5
)
if (d < 20) {
polys +=
Poly(
Array(
V(
x,
3.2,
z + 1
),
V(
x + 1,
3.2,
z + 1
),
V(
x + 1,
3.2,
z
),
V(
x,
3.2,
z
)
),
0.22,
new Color(
35,
31,
30
)
)
}
}
}
// ==============================================================
// WALLS
// ==============================================================
def renderWalls(
polys: ArrayBuffer[Poly]
): Unit = {
for (
z <- 0 until MAP_H;
x <- 0 until MAP_W
if world(z)(x)
) {
if (
distance(
px,
pz,
x + 0.5,
z + 0.5
) < 23
) {
val base =
if (
(x * 13 + z * 7) % 3 == 0
)
new Color(
93,
84,
73
)
else
new Color(
82,
77,
70
)
cube(
polys,
x + 0.5,
1.6,
z + 0.5,
1.0,
3.2,
1.0,
base
)
}
}
}
// ==============================================================
// FURNITURE
// ==============================================================
def renderFurniture(
polys: ArrayBuffer[Poly]
): Unit = {
for (h <- hidingPlaces) {
cube(
polys,
h.x,
1.35,
h.z,
0.95,
2.7,
0.80,
new Color(
57,
45,
37
)
)
cube(
polys,
h.x,
2.77,
h.z,
1.05,
0.08,
0.86,
new Color(
103,
82,
64
)
)
cube(
polys,
h.x + 0.48,
1.25,
h.z,
0.07,
2.45,
0.58,
new Color(
37,
29,
24
)
)
}
val crates = Array(
(4.8, 20.8, 1.3, 0.9),
(12.6, 4.4, 0.9, 0.9),
(20.0, 10.8, 1.0, 0.8),
(26.4, 5.0, 1.1, 0.9)
)
for (c <- crates) {
cube(
polys,
c._1,
0.62,
c._2,
c._3,
1.24,
c._4,
new Color(
75,
54,
38
)
)
}
}
// ==============================================================
// ITEMS
// ==============================================================
def renderItems(
polys: ArrayBuffer[Poly]
): Unit = {
val t =
System.nanoTime()
.toDouble /
280000000.0
for (
it <- items
if !it.collected
) {
if (
distance(
px,
pz,
it.x,
it.z
) < 18
) {
val bob =
math.sin(
t +
it.x * 0.4
) * 0.08
val col =
it.kind match {
case 0 =>
new Color(
217,
169,
63
)
case 1 =>
new Color(
77,
151,
225
)
case _ =>
new Color(
188,
119,
67
)
}
cube(
polys,
it.x,
0.55 + bob,
it.z,
0.30,
0.30,
0.30,
col
)
}
}
}
// ==============================================================
// ENEMY
// ==============================================================
def renderEnemy(
polys: ArrayBuffer[Poly]
): Unit = {
if (
distance(
px,
pz,
enemyX,
enemyZ
) < 22
) {
val alert =
enemyMode == "CHASE"
cube(
polys,
enemyX,
0.98,
enemyZ,
0.72,
1.45,
0.55,
if (alert)
new Color(
63,
41,
43
)
else
new Color(
45,
43,
47
)
)
cube(
polys,
enemyX,
2.02,
enemyZ,
0.54,
0.54,
0.50,
new Color(
164,
140,
122
)
)
// Eyes
cube(
polys,
enemyX - 0.13,
2.08,
enemyZ + 0.23,
0.09,
0.09,
0.06,
new Color(
220,
65,
65
)
)
cube(
polys,
enemyX + 0.13,
2.08,
enemyZ + 0.23,
0.09,
0.09,
0.06,
new Color(
220,
65,
65
)
)
// Shoulders
cube(
polys,
enemyX - 0.47,
1.15,
enemyZ,
0.22,
1.08,
0.48,
new Color(
50,
48,
52
)
)
cube(
polys,
enemyX + 0.47,
1.15,
enemyZ,
0.22,
1.08,
0.48,
new Color(
50,
48,
52
)
)
}
}
// ==============================================================
// EXIT
// ==============================================================
def renderExit(
polys: ArrayBuffer[Poly]
): Unit = {
cube(
polys,
14.5,
1.6,
24.35,
2.3,
3.2,
0.20,
new Color(
68,
68,
75
)
)
val unlocked =
items.forall(
_.collected
)
cube(
polys,
14.5,
1.55,
24.22,
1.9,
2.9,
0.10,
if (unlocked)
new Color(
75,
160,
92
)
else
new Color(
135,
49,
49
)
)
}
// ==============================================================
// SHADING
// ==============================================================
def shaded(
c: Color,
s: Double
): Color = {
def clamp(
v: Int
): Int = {
math.max(
0,
math.min(
255,
v
)
)
}
new Color(
clamp(
(c.getRed * s).toInt
),
clamp(
(c.getGreen * s).toInt
),
clamp(
(c.getBlue * s).toInt
)
)
}
// ==============================================================
// WORLD RENDER
// ==============================================================
def renderWorld(
g: Graphics2D
): Unit = {
val polys =
ArrayBuffer[Poly]()
renderFloor(polys)
renderCeiling(polys)
renderWalls(polys)
renderFurniture(polys)
renderExit(polys)
renderItems(polys)
renderEnemy(polys)
val sorted =
polys.sortBy { p =>
val proj =
p.pts.flatMap(
project
)
if (proj.nonEmpty)
proj.map(
_.depth
).sum /
proj.length
else
9999.0
}.reverse
for (p <- sorted) {
val projected =
p.pts.flatMap(
project
)
if (
projected.length ==
p.pts.length
) {
val poly =
new Polygon()
projected.foreach { q =>
poly.addPoint(
q.x,
q.y
)
}
g.setColor(
shaded(
p.col,
p.shade
)
)
g.fillPolygon(poly)
g.setColor(
new Color(
8,
8,
10,
135
)
)
g.drawPolygon(poly)
}
}
}
// ==============================================================
// ATMOSPHERE
// ==============================================================
def atmosphere(
g: Graphics2D
): Unit = {
val w = getWidth
val h = getHeight
if (hidden) {
g.setColor(
new Color(
0,
0,
0,
238
)
)
g.fillRect(
0,
0,
w,
h
)
g.setColor(
new Color(
145,
116,
90,
110
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
20
)
)
g.drawString(
"Inside the hiding place...",
28,
h - 80
)
} else {
val flicker =
1.0 +
math.sin(
System.nanoTime()
.toDouble /
120000000.0
) * 0.03
val radius =
if (flashlight)
math.min(w, h) *
0.48 *
flicker
else
math.min(w, h) *
0.24
val paint =
new RadialGradientPaint(
new Point2D.Float(
w / 2f,
h / 2f
),
radius.toFloat,
Array(
0.0f,
0.32f,
0.72f,
1.0f
),
Array(
new Color(
0,
0,
0,
0
),
new Color(
0,
0,
0,
25
),
new Color(
0,
0,
0,
138
),
new Color(
0,
0,
0,
if (flashlight)
210
else
250
)
)
)
g.setPaint(paint)
g.fillRect(
0,
0,
w,
h
)
}
}
// ==============================================================
// CROSSHAIR
// ==============================================================
def crosshair(
g: Graphics2D
): Unit = {
if (
!started ||
gameOver ||
escaped
) {
return
}
val cx =
getWidth / 2
val cy =
getHeight / 2
g.setColor(
new Color(
245,
245,
245,
185
)
)
g.setStroke(
new BasicStroke(
1.5f
)
)
g.drawLine(
cx - 7,
cy,
cx + 7,
cy
)
g.drawLine(
cx,
cy - 7,
cx,
cy + 7
)
}
// ==============================================================
// HUD
// ==============================================================
def hud(
g: Graphics2D
): Unit = {
val w = getWidth
val h = getHeight
// ------------------------------------------------------------
// START SCREEN
// ------------------------------------------------------------
if (!started) {
g.setColor(Color.WHITE)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
52
)
)
g.drawString(
"OLD HOUSE ESCAPE 3D",
60,
105
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
20
)
)
g.drawString(
"Original first-person stealth horror game",
62,
145
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
28
)
)
g.drawString(
"PRESS ENTER TO START",
62,
220
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
17
)
)
g.drawString(
"WASD Move | SHIFT Sprint | C Crouch | Mouse Look",
62,
270
)
g.drawString(
"E Interact | F Flashlight | Arrow Keys Look | ESC Quit",
62,
298
)
g.drawString(
"Collect the 3 objects, stay hidden, and escape through the south exit.",
62,
326
)
g.setColor(
new Color(
255,
255,
255,
110
)
)
g.drawString(
"No external assets required.",
62,
365
)
return
}
// ------------------------------------------------------------
// TOP LEFT INFO
// ------------------------------------------------------------
g.setColor(Color.WHITE)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
17
)
)
val secs =
math.max(
0,
nightTime.toInt
)
val mm =
secs / 60
val ss =
secs % 60
val count =
items.count(
_.collected
)
g.drawString(
"NIGHT " +
night +
" / " +
MAX_NIGHTS,
20,
28
)
g.drawString(
"TIME %02d:%02d".format(
mm,
ss
),
20,
53
)
g.drawString(
"OBJECTS " +
count +
" / 3",
20,
78
)
g.drawString(
"HUNTER " +
enemyMode,
20,
103
)
g.drawString(
"STAMINA",
20,
128
)
// Stamina background
g.setColor(
new Color(
30,
30,
34,
190
)
)
g.fillRoundRect(
20,
136,
180,
12,
7,
7
)
// Stamina amount
g.setColor(
new Color(
200,
200,
210,
220
)
)
g.fillRoundRect(
21,
137,
math.max(
0,
math.min(
178,
(sprintEnergy * 1.78).toInt
)
),
10,
6,
6
)
// ------------------------------------------------------------
// OBJECTIVE
// ------------------------------------------------------------
g.setColor(Color.WHITE)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
18
)
)
val objective =
if (count < 3)
"OBJECTIVE: FIND ALL THREE OBJECTS"
else
"OBJECTIVE: REACH THE SOUTH EXIT"
g.drawString(
objective,
20,
h - 48
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
14
)
)
g.drawString(
"WASD Move | SHIFT Sprint | C Crouch | E Interact | F Light | Mouse Look",
20,
h - 20
)
// ------------------------------------------------------------
// HIDDEN
// ------------------------------------------------------------
if (hidden) {
g.setColor(
new Color(
180,
225,
190
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
19
)
)
g.drawString(
"HIDDEN",
w - 105,
30
)
}
// ------------------------------------------------------------
// NOISE
// ------------------------------------------------------------
if (
noisePower > 0 &&
!hidden
) {
g.setColor(
new Color(
235,
205,
125,
210
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
15
)
)
g.drawString(
"NOISE DETECTED",
w - 165,
55
)
}
// ------------------------------------------------------------
// MESSAGE
// ------------------------------------------------------------
if (
msgTime > 0 ||
gameOver ||
escaped
) {
val bw =
math.min(
920,
w - 80
)
val bh = 58
val bx =
(w - bw) / 2
val by =
h - 128
g.setColor(
new Color(
0,
0,
0,
180
)
)
g.fillRoundRect(
bx,
by,
bw,
bh,
14,
14
)
g.setColor(Color.WHITE)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
16
)
)
val tw =
g.getFontMetrics
.stringWidth(msg)
g.drawString(
msg,
bx + (bw - tw) / 2,
by + 36
)
}
// ------------------------------------------------------------
// END SCREEN
// ------------------------------------------------------------
if (
gameOver ||
escaped
) {
g.setColor(
new Color(
0,
0,
0,
185
)
)
g.fillRect(
0,
0,
w,
h
)
g.setColor(Color.WHITE)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
48
)
)
val title =
if (escaped)
"YOU ESCAPED!"
else
"GAME OVER"
val tw =
g.getFontMetrics
.stringWidth(title)
g.drawString(
title,
(w - tw) / 2,
h / 2 - 5
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
19
)
)
val sub =
"PRESS ENTER TO RESTART"
val sw =
g.getFontMetrics
.stringWidth(sub)
g.drawString(
sub,
(w - sw) / 2,
h / 2 + 38
)
}
}
// ==============================================================
// PAINT COMPONENT
// ==============================================================
override def paintComponent(
gg: Graphics
): Unit = {
super.paintComponent(gg)
val g =
gg.asInstanceOf[Graphics2D]
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
)
// Sky
val sky =
new GradientPaint(
0,
0,
new Color(
13,
16,
26
),
0,
getHeight,
new Color(
2,
3,
6
)
)
g.setPaint(sky)
g.fillRect(
0,
0,
getWidth,
getHeight
)
if (started) {
renderWorld(g)
}
atmosphere(g)
crosshair(g)
hud(g)
}
// ==============================================================
// KEYBOARD INPUT
// ==============================================================
override def keyPressed(
e: KeyEvent
): Unit = {
if (
e.getKeyCode ==
KeyEvent.VK_ESCAPE
) {
timer.stop()
val window =
SwingUtilities
.getWindowAncestor(this)
if (window != null) {
window.dispose()
}
return
}
held += e.getKeyCode
if (
e.getKeyCode ==
KeyEvent.VK_E
) {
interactPressed = true
}
requestFocusInWindow()
}
override def keyReleased(
e: KeyEvent
): Unit = {
held -= e.getKeyCode
}
override def keyTyped(
e: KeyEvent
): Unit = {}
// ==============================================================
// MOUSE INPUT
// ==============================================================
override def mouseEntered(
e: MouseEvent
): Unit = {
mouseInside = true
lastMouseX = e.getX
lastMouseY = e.getY
requestFocusInWindow()
}
override def mouseExited(
e: MouseEvent
): Unit = {
mouseInside = false
}
override def mousePressed(
e: MouseEvent
): Unit = {
requestFocusInWindow()
lastMouseX = e.getX
lastMouseY = e.getY
}
override def mouseReleased(
e: MouseEvent
): Unit = {}
override def mouseClicked(
e: MouseEvent
): Unit = {}
override def mouseMoved(
e: MouseEvent
): Unit = {
if (
mouseInside &&
lastMouseX >= 0 &&
started &&
!hidden &&
!gameOver &&
!escaped
) {
val dx =
e.getX - lastMouseX
val dy =
e.getY - lastMouseY
yaw +=
dx * 0.0042
pitch =
math.max(
-0.45,
math.min(
0.45,
pitch +
dy * 0.0018
)
)
}
lastMouseX = e.getX
lastMouseY = e.getY
}
override def mouseDragged(
e: MouseEvent
): Unit = {
mouseMoved(e)
}
}
// ================================================================
// GAME LAUNCHER
// ================================================================
def launchGame(): Unit = {
SwingUtilities.invokeLater(
new Runnable {
override def run(): Unit = {
try {
val frame =
new JFrame(
"Old House Escape 3D"
)
frame.setDefaultCloseOperation(
WindowConstants.EXIT_ON_CLOSE
)
frame.setResizable(false)
val game =
new Game()
frame.setContentPane(game)
// Explicit size so the Kojo window is not 0x0
frame.setSize(
new Dimension(
1280,
760
)
)
frame.setMinimumSize(
new Dimension(
1000,
600
)
)
frame.setLocationRelativeTo(null)
frame.setVisible(true)
// Strong focus request
game.setFocusable(true)
game.requestFocusInWindow()
game.grabFocus()
game.repaint()
} catch {
case ex: Throwable =>
ex.printStackTrace()
JOptionPane.showMessageDialog(
null,
"Could not start Old House Escape 3D.\n\n" +
ex.getClass.getName +
"\n" +
Option(ex.getMessage)
.getOrElse("Unknown error"),
"Game Error",
JOptionPane.ERROR_MESSAGE
)
}
}
}
)
}
}
// ================================================================
// IMPORTANT:
// This explicit call makes it work from Kojo Script Editor.
// ================================================================
OldHouseEscape3D.launchGame()