Code Sketch
horror game
Category: Programming
import javax.swing._
import java.awt._
import java.awt.event._
import java.awt.geom._
import java.io._
import java.nio.file._
import scala.collection.mutable.ArrayBuffer
val GAME_W = 1440
val GAME_H = 860
val VIEW_H = 700
val MAP_W = 48
val MAP_H = 32
val FOV = Math.PI / 3.0
val HALF_FOV = FOV / 2.0
val RAYS = 360
val MAX_DEPTH = 38.0
val PI2 = Math.PI * 2.0
val PLAYER_RADIUS = 0.20
val WALK_SPEED = 0.050
val SPRINT_SPEED = 0.092
val STRAFE_SPEED = 0.045
val ROT_SPEED = 0.050
val MOUSE_SENS = 0.0065
val MAX_TILT = 0.34
val FLOOR_DETAIL = 24
val frame = new JFrame(
"OLD HOUSE: NIGHT WATCH - 3D HORROR ESCAPE OMEGA V5"
)
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.setSize(GAME_W, GAME_H)
frame.setResizable(false)
frame.setLocationRelativeTo(null)
var state = "MENU"
var difficulty = "NORMAL"
var paused = false
var muted = false
var showMap = true
var showHelp = false
var showBlueprint = false
var flashlightOn = true
var hidden = false
var running = true
var gameTime = 0.0
var bestTime = 0.0
var score = 0
var health = 100.0
var stamina = 100.0
var battery = 100.0
var fear = 0.0
var noiseLevel = 0.0
var message =
"Find the house key. The front gate is deadlocked."
var messageTicks = 360
var animTick = 0L
// FIX: val -> var
var mouseDown = false
var mouseLastX = 0
var difficultyScale = 1.0
var enemySpeedScale = 1.0
var detectionScale = 1.0
var batteryDrain = 0.023
var lastSoundTick = -100L
var px = 4.5
var py = 27.5
var pa = -Math.PI / 2.0
var ex = 30.5
var ey = 4.5
var ea = Math.PI
var enemyState = "PATROL"
var enemyTargetX = 30.5
var enemyTargetY = 4.5
var enemyDistance = 99.0
var enemyVisible = false
var enemyStun = 0.0
var enemySearch = 0.0
var enemyWander = 0
var lastNoiseX = 6.0
var lastNoiseY = 26.0
var lastPlayerX = px
var lastPlayerY = py
var scareFlash = 0
var lightning = 0
var doorPulse = 0
var exitGlow = 0
var crouched = false
var headBob = 0.0
var cameraBobX = 0.0
var cameraBobY = 0.0
var footstepPulse = 0.0
var screenShake = 0.0
var interactionPulse = 0.0
var discoveredRooms =
Array.fill[Boolean](20)(false)
var totalDiscoveries = 0
var flashlightFlicker = 0.0
var ambientPulse = 0.0
var doorAnim = 0.0
var enemyAlpha = 245
var interactMode = "NONE"
var cameraRoll = 0.0
var screenGrain = 0.0
var tutorialStep = 0
var hasSeenOpening = false
// FIX: global sprint state so Timer can access it
var sprintingNow = false
var keyFound = false
var fuseFound = false
var crowbarFound = false
var powerOn = false
var cardFound = false
var noteFound = false
var codeA = false
var codeB = false
var codeSolved = false
var handleFound = false
var finalGateOpen = false
var batteryFound = false
var kitchenDoorOpen = false
var basementDoorOpen = false
var eastDoorOpen = false
var safeRoomOpen = false
val keys =
Array.fill[Boolean](512)(false)
val zBuffer =
Array.fill[Double](RAYS)(MAX_DEPTH)
val rainX =
Array.fill[Double](110)(0.0)
val rainY =
Array.fill[Double](110)(0.0)
val rainLen =
Array.fill[Double](110)(0.0)
val dustX =
Array.fill[Double](55)(0.0)
val dustY =
Array.fill[Double](55)(0.0)
val mapTemplate = Array(
"################################################".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........D........##........##........##.....##".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"####.##################################.########".toCharArray,
"#........D...........D.............D...........#".toCharArray,
"####.################.##################.#######".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........D........##........##........##.....##".toCharArray,
"####.################.##################.#######".toCharArray,
"#...........##......................##........##".toCharArray,
"#...........##......................##........##".toCharArray,
"#...........##......................##........##".toCharArray,
"#...........D.......................D........###".toCharArray,
"#...........##......................##........##".toCharArray,
"#...........##......................##........##".toCharArray,
"####.################.##################.#######".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##....E#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"##........##........##........##........##.....#".toCharArray,
"################################################".toCharArray
)
val map = mapTemplate.map(_.clone)
val roomNames = Array(
"West Kitchen",
"Central Dining",
"North Library",
"North Study",
"East Guest Room",
"East Gallery",
"West Bedroom",
"West Washroom",
"Grand Hall",
"Upper Landing",
"Master Bedroom",
"Old Nursery",
"South Workshop",
"South Storage",
"Generator Room",
"Basement Hub",
"Hidden Safe Room",
"East Utility",
"Front Foyer",
"Rear Passage"
)
val roomX = Array(
2, 12, 22, 32, 42,
32, 2, 12, 4, 14,
24, 34, 2, 12, 22,
32, 40, 40, 4, 28
)
val roomY = Array(
1, 1, 1, 1, 1,
1, 11, 11, 9, 9,
9, 9, 24, 24, 24,
24, 18, 18, 27, 18
)
val roomW = Array(
8, 8, 8, 8, 5,
8, 8, 8, 24, 8,
8, 8, 8, 8, 8,
8, 6, 6, 8, 10
)
val roomH = Array(
6, 6, 6, 6, 6,
6, 6, 6, 5, 6,
6, 6, 6, 6, 6,
6, 6, 5, 4, 5
)
val patrolX = Array(
4.5, 16.5, 26.5, 36.5,
44.0, 36.5, 16.5, 6.5,
28.5, 42.0, 30.5, 22.0
)
val patrolY = Array(
4.0, 4.0, 4.0, 4.0,
4.0, 14.0, 14.0, 14.0,
11.5, 22.0, 26.0, 26.0
)
val keyX = 5.6
val keyY = 3.8
val fuseX = 25.7
val fuseY = 26.1
val crowbarX = 15.5
val crowbarY = 26.4
val cardX = 44.0
val cardY = 14.2
val noteX = 36.6
val noteY = 3.9
val codeAX = 23.6
val codeAY = 14.3
val codeBX = 43.5
val codeBY = 26.2
val handleX = 41.8
val handleY = 20.7
val batteryX = 6.2
val batteryY = 28.1
val generatorX = 26.5
val generatorY = 27.0
val exitX = 46.1
val exitY = 27.5
val kitchenDoorX = 10.0
val kitchenDoorY = 5.0
val basementDoorX = 31.0
val basementDoorY = 24.0
val eastDoorX = 40.0
val eastDoorY = 20.0
val safeRoomX = 42.0
val safeRoomY = 20.5
val securityX = 45.0
val securityY = 22.0
val hideX = Array(
7.4,
43.0,
18.5,
5.4,
34.5
)
val hideY = Array(
13.3,
13.0,
15.0,
26.0,
27.2
)
val propX = Array(
3.0, 6.5, 14.0, 17.0, 24.0, 27.5, 34.5, 37.5, 43.0, 45.0,
3.0, 6.8, 14.2, 17.4, 24.2, 27.6, 34.3, 37.4, 43.2, 45.0,
3.0, 6.6, 14.0, 17.2, 24.0, 27.4, 34.4, 37.6, 43.0, 45.0,
4.0, 7.0, 14.0, 17.0, 24.0, 27.0, 34.0, 37.0, 43.0, 45.0
)
val propY = Array(
2.8, 5.2, 2.8, 5.2, 2.8, 5.2, 2.8, 5.2, 2.8, 5.2,
11.8, 14.2, 11.8, 14.2, 11.8, 14.2, 11.8, 14.2, 11.8, 14.2,
25.0, 28.2, 25.0, 28.2, 25.0, 28.2, 25.0, 28.2, 25.0, 28.2,
19.0, 22.0, 19.0, 22.0, 19.0, 22.0, 19.0, 22.0, 19.0, 22.0
)
val propKind = Array(
"TABLE", "SHELF", "BED", "DESK", "TABLE", "OVEN", "PICTURE", "PIPE", "BED", "SHELF",
"BED", "PICTURE", "SHELF", "BED", "PIPE", "DESK", "BED", "PICTURE", "BED", "BARREL",
"CRATE", "CRATE", "GENERATOR", "BARREL", "CRATE", "CRATE", "PIPE", "GENERATOR", "SHELF", "BARREL",
"DESK", "SHELF", "CRATE", "PIPE", "SINK", "TABLE", "PICTURE", "SHELF", "BED", "CRATE"
)
val lampX = Array(
5.0, 15.0, 25.0, 35.0, 44.0,
7.0, 17.0, 27.0, 37.0, 45.0,
6.0, 16.0, 26.0, 36.0, 44.0
)
val lampY = Array(
2.2, 2.2, 2.2, 2.2, 2.2,
12.2, 12.2, 12.2, 12.2, 12.2,
25.2, 25.2, 25.2, 25.2, 25.2
)
val lampKind = Array(
"LAMP", "LAMP", "LAMP", "LAMP", "LAMP",
"LAMP", "LAMP", "LAMP", "LAMP", "LAMP",
"LAMP", "LAMP", "LAMP", "LAMP", "LAMP"
)
val wallMarks = Array(
Array(6.5, 1.02, 0),
Array(16.5, 1.02, 0),
Array(26.5, 1.02, 0),
Array(36.5, 1.02, 0),
Array(44.5, 1.02, 0),
Array(2.02, 4.8, 1),
Array(12.02, 4.8, 1),
Array(22.02, 4.8, 1),
Array(32.02, 4.8, 1),
Array(42.02, 4.8, 1),
Array(6.5, 30.98, 0),
Array(16.5, 30.98, 0),
Array(26.5, 30.98, 0),
Array(36.5, 30.98, 0),
Array(44.5, 30.98, 0)
)
var objectiveIndex = 0
// ---------------------------------------------------------------------------
// BASIC HELPERS
// ---------------------------------------------------------------------------
def clamp(
v: Double,
lo: Double,
hi: Double
): Double = {
Math.max(lo, Math.min(hi, v))
}
def dist(
ax: Double,
ay: Double,
bx: Double,
by: Double
): Double = {
val dx = ax - bx
val dy = ay - by
Math.sqrt(
dx * dx + dy * dy
)
}
def angleNorm(
v: Double
): Double = {
var a = v
while (a > Math.PI) {
a -= PI2
}
while (a < -Math.PI) {
a += PI2
}
a
}
def fmt1(
v: Double
): String = {
(
Math.round(
v * 10.0
).toDouble / 10.0
).toString
}
def fmtTime(
t: Double
): String = {
val total =
Math.max(
0,
t.toInt
)
val m =
total / 60
val s =
total % 60
(
if (m < 10) "0" else ""
) +
m +
":" +
(
if (s < 10) "0" else ""
) +
s
}
def setMessage(
s: String,
ticks: Int
): Unit = {
message = s
messageTicks = ticks
}
def roomNameAt(
x: Double,
y: Double
): String = {
var i = 0
while (i < roomNames.length) {
if (
x >= roomX(i) &&
x <= roomX(i) + roomW(i) &&
y >= roomY(i) &&
y <= roomY(i) + roomH(i)
) {
return roomNames(i)
}
i += 1
}
if (
y >= 8.2 &&
y <= 9.4
) {
"North Corridor"
}
else if (
y >= 16.2 &&
y <= 17.3
) {
"Central Corridor"
}
else if (
y >= 23.3 &&
y <= 24.5
) {
"South Corridor"
}
else {
"Connecting Passage"
}
}
def currentObjective(): String = {
if (!keyFound) {
"Find the brass house key in the West Kitchen."
}
else if (!kitchenDoorOpen) {
"Use the brass key at the kitchen service door."
}
else if (!crowbarFound) {
"Find the old crowbar in South Storage."
}
else if (!fuseFound) {
"Find the emergency fuse in the Generator Room."
}
else if (!powerOn) {
"Install the fuse and restore the generator."
}
else if (!cardFound) {
"Find the blue access card in the East Gallery."
}
else if (!noteFound) {
"Find the security note in the North Study."
}
else if (!codeA || !codeB) {
"Find both security-code fragments."
}
else if (!codeSolved) {
"Use the security terminal and enter the 4-digit code."
}
else if (!handleFound) {
"Find the old gate handle in the Hidden Safe Room."
}
else {
"Return to the front exit and escape the house."
}
}
def allObjectives(): Boolean = {
keyFound &&
kitchenDoorOpen &&
crowbarFound &&
fuseFound &&
powerOn &&
cardFound &&
noteFound &&
codeA &&
codeB &&
codeSolved &&
handleFound
}
// ---------------------------------------------------------------------------
// WORLD / COLLISION
// ---------------------------------------------------------------------------
def cellAt(
x: Int,
y: Int
): Char = {
if (
x < 0 ||
y < 0 ||
x >= MAP_W ||
y >= MAP_H
) {
'#'
}
else {
map(y)(x)
}
}
def doorAt(
x: Int,
y: Int
): Boolean = {
(
x == 10 && y == 5
) ||
(
x == 10 && y == 15
) ||
(
x == 10 && y == 21
) ||
(
x == 20 && y == 9
) ||
(
x == 20 && y == 16
) ||
(
x == 20 && y == 23
) ||
(
x == 30 && y == 9
) ||
(
x == 30 && y == 16
) ||
(
x == 30 && y == 23
) ||
(
x == 40 && y == 9
) ||
(
x == 40 && y == 16
) ||
(
x == 40 && y == 20
) ||
(
x == 10 && y == 24
) ||
(
x == 20 && y == 24
) ||
(
x == 30 && y == 24
) ||
(
x == 40 && y == 24
)
}
def doorOpenAt(
x: Int,
y: Int
): Boolean = {
if (
(x == 10 && y == 5) ||
(x == 10 && y == 15) ||
(x == 10 && y == 21)
) {
kitchenDoorOpen
}
else if (
x == 40 &&
y == 20
) {
eastDoorOpen
}
else if (
x == 30 &&
y == 24
) {
basementDoorOpen
}
else {
safeRoomOpen ||
kitchenDoorOpen
}
}
def blockedCell(
x: Int,
y: Int
): Boolean = {
val c =
cellAt(x, y)
if (c == '#') {
true
}
else if (
c == 'E' &&
!finalGateOpen
) {
true
}
else if (
doorAt(x, y) &&
!doorOpenAt(x, y)
) {
true
}
else {
false
}
}
def blocked(
x: Double,
y: Double
): Boolean = {
val r =
PLAYER_RADIUS
blockedCell(
Math.floor(x - r).toInt,
Math.floor(y - r).toInt
) ||
blockedCell(
Math.floor(x + r).toInt,
Math.floor(y - r).toInt
) ||
blockedCell(
Math.floor(x - r).toInt,
Math.floor(y + r).toInt
) ||
blockedCell(
Math.floor(x + r).toInt,
Math.floor(y + r).toInt
)
}
def moveEntity(
x: Double,
y: Double,
nx: Double,
ny: Double
): (Double, Double) = {
var xx = nx
var yy = ny
if (blocked(xx, y)) {
xx = x
}
if (blocked(xx, yy)) {
yy = y
}
(xx, yy)
}
def lineOfSight(
ax: Double,
ay: Double,
bx: Double,
by: Double
): Boolean = {
val d =
dist(
ax,
ay,
bx,
by
)
val steps =
Math.max(
1,
(d / 0.06).toInt
)
var i = 1
while (i < steps) {
val t =
i.toDouble /
steps.toDouble
val x =
ax +
(bx - ax) * t
val y =
ay +
(by - ay) * t
if (
blockedCell(
Math.floor(x).toInt,
Math.floor(y).toInt
)
) {
return false
}
i += 1
}
true
}
// ---------------------------------------------------------------------------
// SAVE
// ---------------------------------------------------------------------------
def loadBestTime(): Double = {
try {
val p =
Paths.get(
"old_house_night_watch_best_time.txt"
)
if (Files.exists(p)) {
new String(
Files.readAllBytes(p),
"UTF-8"
).trim.toDouble
}
else {
0.0
}
}
catch {
case _: Exception => 0.0
}
}
def saveBestTime(
v: Double
): Unit = {
try {
Files.write(
Paths.get(
"old_house_night_watch_best_time.txt"
),
v.toString.getBytes("UTF-8")
)
}
catch {
case _: Exception =>
}
}
bestTime =
loadBestTime()
def safeTone(): Unit = {
if (
muted ||
animTick -
lastSoundTick < 7L
) {
return
}
lastSoundTick =
animTick
try {
Toolkit
.getDefaultToolkit
.beep()
}
catch {
case _: Exception =>
}
}
// ---------------------------------------------------------------------------
// RESET / GAME START
// ---------------------------------------------------------------------------
def resetWorld(): Unit = {
var y = 0
while (y < MAP_H) {
var x = 0
while (x < MAP_W) {
map(y)(x) =
mapTemplate(y)(x)
x += 1
}
y += 1
}
px = 4.5
py = 27.5
pa = -Math.PI / 2.0
ex = 30.5
ey = 4.5
ea = Math.PI
enemyState = "PATROL"
enemyTargetX =
patrolX(0)
enemyTargetY =
patrolY(0)
enemyDistance = 99.0
enemyVisible = false
enemyStun = 0.0
enemySearch = 0.0
enemyWander = 0
keyFound = false
fuseFound = false
crowbarFound = false
powerOn = false
cardFound = false
noteFound = false
codeA = false
codeB = false
codeSolved = false
handleFound = false
finalGateOpen = false
batteryFound = false
kitchenDoorOpen = false
basementDoorOpen = false
eastDoorOpen = false
safeRoomOpen = false
hidden = false
gameTime = 0.0
score = 0
health = 100.0
stamina = 100.0
battery = 100.0
fear = 0.0
noiseLevel = 0.0
message =
"You wake in the rear foyer. Find a way through the old house."
messageTicks = 360
paused = false
running = true
scareFlash = 0
lightning = 0
doorPulse = 0
exitGlow = 0
objectiveIndex = 0
crouched = false
sprintingNow = false
headBob = 0.0
cameraBobX = 0.0
cameraBobY = 0.0
footstepPulse = 0.0
screenShake = 0.0
interactionPulse = 0.0
totalDiscoveries = 0
var di = 0
while (
di < discoveredRooms.length
) {
discoveredRooms(di) = false
di += 1
}
lastNoiseX = px
lastNoiseY = py
lastPlayerX = px
lastPlayerY = py
}
def beginGame(): Unit = {
resetWorld()
state = "PLAYING"
safeTone()
}
def setDifficulty(
name: String
): Unit = {
difficulty = name
if (name == "EASY") {
difficultyScale = 0.80
enemySpeedScale = 0.82
detectionScale = 0.74
batteryDrain = 0.017
}
else if (name == "HARD") {
difficultyScale = 1.20
enemySpeedScale = 1.20
detectionScale = 1.28
batteryDrain = 0.030
}
else {
difficultyScale = 1.0
enemySpeedScale = 1.0
detectionScale = 1.0
batteryDrain = 0.023
}
setMessage(
"Difficulty: " + name,
140
)
}
// ---------------------------------------------------------------------------
// PICKUPS / PUZZLES
// ---------------------------------------------------------------------------
def collectNearby(): Unit = {
if (
!keyFound &&
dist(
px,
py,
keyX,
keyY
) < 0.72
) {
keyFound = true
score += 100
safeTone()
setMessage(
"BRASS KEY FOUND. A service door can now be opened.",
220
)
}
if (
!crowbarFound &&
kitchenDoorOpen &&
dist(
px,
py,
crowbarX,
crowbarY
) < 0.72
) {
crowbarFound = true
score += 110
safeTone()
setMessage(
"CROWBAR FOUND. Metal panels can be opened.",
200
)
}
if (
!fuseFound &&
crowbarFound &&
dist(
px,
py,
fuseX,
fuseY
) < 0.72
) {
fuseFound = true
score += 120
safeTone()
setMessage(
"EMERGENCY FUSE FOUND.",
170
)
}
if (
!cardFound &&
powerOn &&
dist(
px,
py,
cardX,
cardY
) < 0.72
) {
cardFound = true
score += 150
safeTone()
setMessage(
"BLUE ACCESS CARD FOUND.",
170
)
}
if (
!noteFound &&
powerOn &&
dist(
px,
py,
noteX,
noteY
) < 0.72
) {
noteFound = true
score += 85
safeTone()
setMessage(
"SECURITY NOTE FOUND. Two fragments remain.",
200
)
}
if (
!codeA &&
powerOn &&
dist(
px,
py,
codeAX,
codeAY
) < 0.72
) {
codeA = true
score += 70
safeTone()
setMessage(
"CODE FRAGMENT A FOUND.",
140
)
}
if (
!codeB &&
powerOn &&
dist(
px,
py,
codeBX,
codeBY
) < 0.72
) {
codeB = true
score += 70
safeTone()
setMessage(
"CODE FRAGMENT B FOUND.",
140
)
}
if (
!handleFound &&
codeSolved &&
cardFound &&
dist(
px,
py,
handleX,
handleY
) < 0.72
) {
handleFound = true
score += 200
safeTone()
setMessage(
"EXIT HANDLE FOUND. Return to the front gate.",
230
)
}
if (
!batteryFound &&
dist(
px,
py,
batteryX,
batteryY
) < 0.72
) {
batteryFound = true
battery = 100.0
score += 30
safeTone()
setMessage(
"Flashlight battery replaced.",
140
)
}
}
def useKitchenDoor(): Unit = {
if (kitchenDoorOpen) {
return
}
if (
dist(
px,
py,
kitchenDoorX,
kitchenDoorY
) < 1.25
) {
if (keyFound) {
kitchenDoorOpen = true
doorPulse = 24
score += 90
safeTone()
setMessage(
"SERVICE DOOR UNLOCKED.",
160
)
}
else {
setMessage(
"Locked. Find the brass key first.",
140
)
safeTone()
}
}
}
def useGenerator(): Unit = {
if (powerOn) {
return
}
if (
dist(
px,
py,
generatorX,
generatorY
) < 1.25
) {
if (
fuseFound &&
crowbarFound
) {
powerOn = true
score += 220
doorPulse = 30
safeTone()
setMessage(
"GENERATOR ONLINE. Security systems are awake.",
260
)
}
else {
setMessage(
"The generator needs an emergency fuse and a tool.",
170
)
}
}
}
def useBasementDoor(): Unit = {
if (basementDoorOpen) {
return
}
if (
dist(
px,
py,
basementDoorX,
basementDoorY
) < 1.30
) {
if (codeSolved) {
basementDoorOpen = true
score += 120
safeTone()
setMessage(
"BASEMENT DOOR UNLOCKED.",
160
)
}
else {
setMessage(
"A security lock blocks this door.",
140
)
}
}
}
def useEastDoor(): Unit = {
if (eastDoorOpen) {
return
}
if (
dist(
px,
py,
eastDoorX,
eastDoorY
) < 1.30
) {
if (cardFound) {
eastDoorOpen = true
safeTone()
setMessage(
"ACCESS CARD ACCEPTED. EAST UTILITY OPEN.",
180
)
}
else {
setMessage(
"Card reader: access denied.",
140
)
}
}
}
def useSafeRoom(): Unit = {
if (safeRoomOpen) {
return
}
if (
dist(
px,
py,
safeRoomX,
safeRoomY
) < 1.35
) {
if (
codeSolved &&
cardFound
) {
safeRoomOpen = true
score += 130
safeTone()
setMessage(
"HIDDEN SAFE ROOM UNSEALED.",
190
)
}
else {
setMessage(
"Two locks are still active here.",
140
)
}
}
}
def solveSecurity(): Unit = {
if (codeSolved) {
return
}
if (
!codeA ||
!codeB ||
!noteFound ||
!cardFound
) {
return
}
if (
dist(
px,
py,
securityX,
securityY
) < 1.35 &&
powerOn
) {
val entry =
JOptionPane.showInputDialog(
frame,
"SECURITY PANEL\nEnter the 4-digit house code from the note and fragments:",
"Security Code",
JOptionPane.QUESTION_MESSAGE
)
if (entry != null) {
if (entry.trim == "5814") {
codeSolved = true
score += 260
safeTone()
setMessage(
"SECURITY CODE ACCEPTED. Restricted doors released.",
260
)
}
else {
fear =
clamp(
fear + 10.0,
0.0,
100.0
)
score =
Math.max(
0,
score - 20
)
noiseLevel = 1.0
lastNoiseX = px
lastNoiseY = py
safeTone()
setMessage(
"Wrong code. An alarm pulse echoed through the house.",
180
)
}
}
}
}
def attemptEscape(): Unit = {
if (
dist(
px,
py,
exitX,
exitY
) < 1.35
) {
if (allObjectives()) {
finalGateOpen = true
state = "WIN"
running = false
val bonus =
Math.max(
0,
(
2400.0 -
gameTime * 18.0
).toInt
)
score += bonus
if (
bestTime == 0.0 ||
gameTime < bestTime
) {
bestTime =
gameTime
saveBestTime(
bestTime
)
}
safeTone()
setMessage(
"YOU ESCAPED THE NIGHT HOUSE.",
300
)
}
else {
setMessage(
"Gate locked. " +
currentObjective(),
190
)
}
}
}
def interact(): Unit = {
collectNearby()
useKitchenDoor()
useGenerator()
useBasementDoor()
useEastDoor()
useSafeRoom()
solveSecurity()
attemptEscape()
}
// ---------------------------------------------------------------------------
// PLAYER / NOISE
// ---------------------------------------------------------------------------
def toggleHide(): Unit = {
var near = false
var i = 0
while (i < hideX.length) {
if (
dist(
px,
py,
hideX(i),
hideY(i)
) < 1.1
) {
near = true
}
i += 1
}
if (near) {
hidden = !hidden
if (hidden) {
setMessage(
"HIDDEN. Stay quiet.",
150
)
}
else {
setMessage(
"You leave hiding.",
100
)
}
}
else {
setMessage(
"No hiding place nearby.",
110
)
}
}
def updatePlayer(
dt: Double
): Unit = {
val forward =
if (
keys(KeyEvent.VK_W) ||
keys(KeyEvent.VK_UP)
) {
1.0
}
else {
0.0
}
val back =
if (
keys(KeyEvent.VK_S) ||
keys(KeyEvent.VK_DOWN)
) {
1.0
}
else {
0.0
}
val left =
if (
keys(KeyEvent.VK_A) ||
keys(KeyEvent.VK_LEFT)
) {
1.0
}
else {
0.0
}
val right =
if (
keys(KeyEvent.VK_D) ||
keys(KeyEvent.VK_RIGHT)
) {
1.0
}
else {
0.0
}
if (
keys(KeyEvent.VK_C) ||
keys(KeyEvent.VK_V)
) {
crouched = true
}
else {
crouched = false
}
val sprinting =
(
keys(KeyEvent.VK_SHIFT) ||
keys(KeyEvent.VK_CONTROL)
) &&
stamina > 3.0 &&
(
forward + back
) != 0.0 &&
!hidden &&
!crouched
// FIX: make current sprint state available outside this method
sprintingNow = sprinting
var speed =
if (sprinting) {
SPRINT_SPEED
}
else {
WALK_SPEED
}
if (crouched) {
speed *= 0.48
}
if (hidden) {
speed *= 0.25
}
var dx = 0.0
var dy = 0.0
dx +=
Math.cos(pa) *
(
forward - back
) *
speed
dy +=
Math.sin(pa) *
(
forward - back
) *
speed
dx +=
Math.cos(
pa + Math.PI / 2.0
) *
(
right - left
) *
STRAFE_SPEED
dy +=
Math.sin(
pa + Math.PI / 2.0
) *
(
right - left
) *
STRAFE_SPEED
val moved =
Math.sqrt(
dx * dx +
dy * dy
)
val newPos =
moveEntity(
px,
py,
px + dx,
py + dy
)
px =
newPos._1
py =
newPos._2
if (
sprinting &&
moved > 0.001
) {
stamina =
clamp(
stamina - 0.62,
0.0,
100.0
)
noiseLevel =
clamp(
noiseLevel + 0.045,
0.0,
1.0
)
}
else if (
crouched &&
moved > 0.001
) {
stamina =
clamp(
stamina + 0.22,
0.0,
100.0
)
noiseLevel =
clamp(
noiseLevel - 0.010,
0.0,
1.0
)
}
else {
stamina =
clamp(
stamina + 0.38,
0.0,
100.0
)
noiseLevel =
clamp(
noiseLevel - 0.025,
0.0,
1.0
)
}
if (
moved > 0.001
) {
lastNoiseX = px
lastNoiseY = py
lastPlayerX = px
lastPlayerY = py
// FIX:
// Scala/Kojo-safe if expression
headBob += (
if (sprinting)
0.24
else
0.12
)
footstepPulse =
clamp(
footstepPulse +
(
if (sprinting)
0.18
else
0.10
),
0.0,
1.0
)
}
else {
footstepPulse =
clamp(
footstepPulse - 0.08,
0.0,
1.0
)
}
cameraBobX =
Math.sin(headBob) *
(
if (crouched)
1.0
else
2.8
)
cameraBobY =
Math.abs(
Math.cos(headBob)
) *
(
if (sprinting)
5.0
else
2.2
)
if (
flashlightOn &&
battery > 0.0
) {
battery =
clamp(
battery - batteryDrain,
0.0,
100.0
)
}
if (
battery <= 0.0
) {
flashlightOn = false
}
if (fear > 0.0) {
fear =
clamp(
fear - 0.02,
0.0,
100.0
)
}
if (
enemyDistance < 5.0 &&
enemyState == "CHASE" &&
!hidden
) {
fear =
clamp(
fear + 0.20,
0.0,
100.0
)
}
if (
enemyState == "CHASE" &&
enemyDistance < 3.0
) {
screenShake =
clamp(
screenShake + 0.9,
0.0,
8.0
)
}
else {
screenShake =
clamp(
screenShake - 0.4,
0.0,
8.0
)
}
if (
messageTicks <= 0
) {
val rn =
roomNameAt(
px,
py
)
var rIndex = 0
while (
rIndex < roomNames.length
) {
if (
rn == roomNames(rIndex) &&
!discoveredRooms(rIndex)
) {
discoveredRooms(rIndex) =
true
totalDiscoveries += 1
if (
totalDiscoveries <=
roomNames.length
) {
score += 5
}
rIndex =
roomNames.length
}
else {
rIndex += 1
}
}
}
if (
health <= 0.0
) {
state = "LOSE"
running = false
}
}
// ---------------------------------------------------------------------------
// ENEMY AI
// ---------------------------------------------------------------------------
def setEnemyTargetToPatrol(): Unit = {
if (
patrolX.length == 0
) {
return
}
val idx =
enemyWander %
patrolX.length
enemyTargetX =
patrolX(idx)
enemyTargetY =
patrolY(idx)
}
def updateEnemy(
dt: Double
): Unit = {
enemyDistance =
dist(
px,
py,
ex,
ey
)
enemyVisible =
lineOfSight(
ex,
ey,
px,
py
) &&
Math.abs(
angleNorm(
Math.atan2(
py - ey,
px - ex
) - ea
)
) <
Math.PI *
(
if (crouched)
0.58
else
0.78
)
if (
enemyStun > 0.0
) {
enemyStun =
Math.max(
0.0,
enemyStun - dt
)
}
if (hidden) {
if (
enemyDistance < 4.0 &&
enemyState == "CHASE"
) {
enemyState = "SEARCH"
enemySearch = 4.5
enemyTargetX = lastNoiseX
enemyTargetY = lastNoiseY
}
}
else {
if (
enemyVisible &&
enemyDistance <
(
if (crouched)
6.4
else
9.5
) *
detectionScale
) {
enemyState = "CHASE"
enemyTargetX = px
enemyTargetY = py
enemySearch = 0.0
}
else if (
noiseLevel > 0.30 &&
enemyDistance <
(
if (crouched)
8.0
else
12.0
) *
detectionScale
) {
enemyState = "SEARCH"
enemySearch = 5.5
enemyTargetX = lastNoiseX
enemyTargetY = lastNoiseY
}
}
if (
enemyState == "SEARCH"
) {
enemySearch -= dt
if (
enemySearch <= 0.0
) {
enemyState = "PATROL"
enemyWander += 1
setEnemyTargetToPatrol()
}
else if (
dist(
ex,
ey,
enemyTargetX,
enemyTargetY
) < 0.7
) {
enemyWander += 1
enemyTargetX =
ex +
Math.cos(
animTick.toDouble *
0.03
) *
2.2
enemyTargetY =
ey +
Math.sin(
animTick.toDouble *
0.027
) *
2.2
}
}
if (
enemyState == "PATROL"
) {
if (
dist(
ex,
ey,
enemyTargetX,
enemyTargetY
) < 0.75
) {
enemyWander += 1
setEnemyTargetToPatrol()
}
}
if (
enemyState == "CHASE"
) {
enemyTargetX = px
enemyTargetY = py
}
val targetAng =
Math.atan2(
enemyTargetY - ey,
enemyTargetX - ex
)
ea =
angleNorm(
ea +
clamp(
angleNorm(
targetAng - ea
),
-ROT_SPEED,
ROT_SPEED
)
)
val chaseSpeed =
if (
enemyState == "CHASE"
) {
0.075 *
enemySpeedScale
}
else if (
enemyState == "SEARCH"
) {
0.054 *
enemySpeedScale
}
else {
0.037 *
enemySpeedScale
}
if (
enemyStun <= 0.0
) {
val nx =
ex +
Math.cos(ea) *
chaseSpeed
val ny =
ey +
Math.sin(ea) *
chaseSpeed
val p =
moveEntity(
ex,
ey,
nx,
ny
)
ex =
p._1
ey =
p._2
}
if (
!hidden &&
enemyDistance < 0.72 &&
enemyStun <= 0.0
) {
health =
clamp(
health -
0.95 *
difficultyScale,
0.0,
100.0
)
fear = 100.0
scareFlash = 18
enemyState = "CHASE"
}
if (
flashlightOn &&
enemyVisible &&
enemyDistance < 6.0 &&
enemyStun <= 0.0 &&
battery > 2.0
) {
val headAng =
Math.atan2(
ey - py,
ex - px
)
val diff =
Math.abs(
angleNorm(
headAng - pa
)
)
if (
diff < 0.18
) {
enemyStun = 2.5
enemyState = "STUNNED"
score += 15
safeTone()
setMessage(
"The flashlight stunned the Watcher briefly.",
100
)
}
}
if (
enemyState == "STUNNED" &&
enemyStun <= 0.0
) {
enemyState = "PATROL"
enemyWander += 1
setEnemyTargetToPatrol()
}
}
// ---------------------------------------------------------------------------
// 3D HELPERS
// ---------------------------------------------------------------------------
def safeDepth(
d: Double
): Double = {
val v =
if (d < 0.05)
0.05
else
d
if (
v.isNaN ||
v.isInfinite
) {
MAX_DEPTH
}
else {
v
}
}
def projectX(
angleOffset: Double
): Double = {
angleOffset
}
def wallShade(
distValue: Double,
hitSide: Int,
wallX: Int,
wallY: Int
): Color = {
val base =
(
150.0 -
distValue * 3.0
).toInt
val variation =
(
(
wallX * 17 +
wallY * 11
) %
24
) -
12
val sideBoost =
if (hitSide == 1)
-18
else
0
val r =
clamp(
(
base +
variation +
sideBoost
).toDouble,
28.0,
150.0
).toInt
val gg =
clamp(
(
base +
variation +
sideBoost +
5
).toDouble,
30.0,
165.0
).toInt
val b =
clamp(
(
base +
variation +
sideBoost +
10
).toDouble,
38.0,
180.0
).toInt
new Color(
r,
gg,
b
)
}
def wallTextureOffset(
hitX: Double,
hitY: Double,
side: Int
): Double = {
val frac =
if (side == 0)
hitY -
Math.floor(hitY)
else
hitX -
Math.floor(hitX)
frac - 0.5
}
def castRay(
rayAngle: Double,
index: Int
): Double = {
val dirX =
Math.cos(rayAngle)
val dirY =
Math.sin(rayAngle)
var mapX =
Math.floor(px).toInt
var mapY =
Math.floor(py).toInt
val deltaDistX =
if (
Math.abs(dirX) <
0.00001
) {
1.0e30
}
else {
Math.abs(
1.0 / dirX
)
}
val deltaDistY =
if (
Math.abs(dirY) <
0.00001
) {
1.0e30
}
else {
Math.abs(
1.0 / dirY
)
}
var stepX = 1
var stepY = 1
var sideDistX = 0.0
var sideDistY = 0.0
if (dirX < 0.0) {
stepX = -1
sideDistX =
(
px -
mapX
) *
deltaDistX
}
else {
sideDistX =
(
mapX +
1.0 -
px
) *
deltaDistX
}
if (dirY < 0.0) {
stepY = -1
sideDistY =
(
py -
mapY
) *
deltaDistY
}
else {
sideDistY =
(
mapY +
1.0 -
py
) *
deltaDistY
}
var hit = false
var side = 0
var depth = 0.0
var loop = 0
while (
!hit &&
depth < MAX_DEPTH &&
loop < 120
) {
if (
sideDistX <
sideDistY
) {
sideDistX +=
deltaDistX
mapX += stepX
side = 0
}
else {
sideDistY +=
deltaDistY
mapY += stepY
side = 1
}
depth =
if (side == 0)
sideDistX -
deltaDistX
else
sideDistY -
deltaDistY
val c =
cellAt(
mapX,
mapY
)
if (
c == '#' ||
c == 'E' ||
(
doorAt(
mapX,
mapY
) &&
!doorOpenAt(
mapX,
mapY
)
)
) {
hit = true
}
loop += 1
}
val safe =
safeDepth(depth)
zBuffer(index) =
safe
safe
}
// ---------------------------------------------------------------------------
// RENDERING
// ---------------------------------------------------------------------------
def drawSkyFloor(
g: Graphics2D
): Unit = {
val h =
VIEW_H
var y = 0
while (
y < h / 2
) {
val t =
y.toDouble /
(
h / 2
).toDouble
val r =
(
8 +
20 * t
).toInt
val gg =
(
12 +
18 * t
).toInt
val b =
(
20 +
22 * t
).toInt
g.setColor(
new Color(
r,
gg,
b
)
)
g.drawLine(
0,
y,
GAME_W,
y
)
y += 1
}
y =
h / 2
while (
y < h
) {
val t =
(
y -
h / 2
).toDouble /
(
h / 2
).toDouble
val c =
(
14 +
24 * t
).toInt
g.setColor(
new Color(
c,
c,
c + 7
)
)
g.drawLine(
0,
y,
GAME_W,
y
)
y += 1
}
}
def drawWallView(
g: Graphics2D
): Unit = {
drawSkyFloor(g)
drawCeilingDetails(g)
drawPerspectiveFloor(g)
var r = 0
while (
r < RAYS
) {
val camera =
r.toDouble /
RAYS.toDouble
val rayAngle =
pa -
HALF_FOV +
camera *
FOV
val corrected =
castRay(
rayAngle,
r
)
val correctedDepth =
safeDepth(
corrected *
Math.cos(
rayAngle - pa
)
)
val lineH =
clamp(
VIEW_H.toDouble /
correctedDepth,
1.0,
VIEW_H.toDouble *
1.6
).toInt
val top =
(
VIEW_H / 2 +
cameraBobY.toInt
) -
lineH / 2
val sx =
(
r.toDouble *
GAME_W.toDouble /
RAYS.toDouble +
cameraBobX
).toInt
val exx =
(
(
r + 1
).toDouble *
GAME_W.toDouble /
RAYS.toDouble +
cameraBobX
).toInt +
1
val midX =
px +
Math.cos(
rayAngle
) *
corrected
val midY =
py +
Math.sin(
rayAngle
) *
corrected
val cellX =
Math.floor(
midX
).toInt
val cellY =
Math.floor(
midY
).toInt
val hitSide =
if (
Math.abs(
Math.sin(rayAngle)
) >
Math.abs(
Math.cos(rayAngle)
)
) {
1
}
else {
0
}
var c =
wallShade(
correctedDepth,
hitSide,
cellX,
cellY
)
if (
flashlightOn &&
battery > 0.0
) {
val centerDiff =
Math.abs(
angleNorm(
rayAngle - pa
)
)
if (
centerDiff < 0.27
) {
val boost =
clamp(
(
1.0 -
correctedDepth /
9.0
) *
70.0,
0.0,
70.0
).toInt
c =
new Color(
Math.min(
220,
c.getRed + boost
),
Math.min(
225,
c.getGreen + boost
),
Math.min(
235,
c.getBlue + boost
)
)
}
}
if (
lightning > 0
) {
c =
new Color(
Math.min(
255,
c.getRed + 80
),
Math.min(
255,
c.getGreen + 80
),
Math.min(
255,
c.getBlue + 85
)
)
}
g.setColor(c)
g.fillRect(
sx,
top,
exx - sx,
lineH
)
if (
r % 9 == 0
) {
val tx =
wallTextureOffset(
midX,
midY,
hitSide
)
val line =
clamp(
(
tx + 0.5
) *
90.0,
0.0,
89.0
).toInt
g.setColor(
new Color(
Math.min(
255,
c.getRed +
line / 8
),
Math.min(
255,
c.getGreen +
line / 10
),
Math.min(
255,
c.getBlue +
line / 12
)
)
)
g.fillRect(
sx,
top + lineH / 3,
Math.max(
1,
exx - sx
),
Math.max(
1,
lineH / 12
)
)
}
r += 1
}
}
def drawBillboard(
g: Graphics2D,
wx: Double,
wy: Double,
kind: String,
size: Double,
glow: Boolean
): Unit = {
val dx =
wx - px
val dy =
wy - py
val d =
Math.sqrt(
dx * dx +
dy * dy
)
val ang =
angleNorm(
Math.atan2(
dy,
dx
) - pa
)
if (
Math.abs(ang) >
HALF_FOV + 0.30
) {
return
}
val depth =
safeDepth(
d *
Math.cos(ang)
)
if (
depth <= 0.15 ||
depth >= MAX_DEPTH
) {
return
}
val screenX =
(
GAME_W / 2.0
) +
Math.tan(ang) *
(
GAME_W / 2.0
) /
Math.tan(HALF_FOV)
val h =
clamp(
size *
VIEW_H.toDouble /
depth,
5.0,
560.0
)
val w =
h * 0.72
val x =
(
screenX -
w / 2.0
).toInt
val y =
(
VIEW_H / 2.0 -
h / 2.0
).toInt
val centerRay =
clamp(
screenX /
GAME_W.toDouble *
RAYS.toDouble,
0.0,
(
RAYS - 1
).toDouble
).toInt
if (
centerRay < 0 ||
centerRay >= RAYS ||
depth >
zBuffer(centerRay) +
0.25
) {
return
}
if (glow) {
g.setColor(
new Color(
235,
220,
120,
90
)
)
g.fillOval(
(
screenX -
h * 0.25
).toInt,
(
VIEW_H / 2 -
h * 0.25
).toInt,
(
h * 0.5
).toInt,
(
h * 0.5
).toInt
)
}
if (
kind == "ENEMY"
) {
g.setColor(
new Color(
10,
10,
12,
245
)
)
g.fillOval(
x +
(
w * 0.24
).toInt,
y,
(
w * 0.52
).toInt,
(
h * 0.30
).toInt
)
g.fillRoundRect(
x +
(
w * 0.18
).toInt,
y +
(
h * 0.20
).toInt,
(
w * 0.64
).toInt,
(
h * 0.78
).toInt,
22,
22
)
g.setColor(
new Color(
215,
215,
205,
230
)
)
g.fillOval(
x +
(
w * 0.34
).toInt,
y +
(
h * 0.09
).toInt,
(
w * 0.11
).toInt,
(
w * 0.11
).toInt
)
g.fillOval(
x +
(
w * 0.55
).toInt,
y +
(
h * 0.09
).toInt,
(
w * 0.11
).toInt,
(
w * 0.11
).toInt
)
if (
enemyState == "CHASE"
) {
g.setColor(
new Color(
210,
50,
60
)
)
g.fillOval(
x +
(
w * 0.37
).toInt,
y +
(
h * 0.15
).toInt,
(
w * 0.08
).toInt,
(
w * 0.05
).toInt
)
g.fillOval(
x +
(
w * 0.54
).toInt,
y +
(
h * 0.15
).toInt,
(
w * 0.08
).toInt,
(
w * 0.05
).toInt
)
}
}
else {
val base =
if (
kind == "KEY"
) {
new Color(
232,
192,
75
)
}
else if (
kind == "FUSE"
) {
new Color(
232,
86,
75
)
}
else if (
kind == "CARD"
) {
new Color(
74,
160,
235
)
}
else if (
kind == "HANDLE"
) {
new Color(
210,
145,
60
)
}
else if (
kind == "BATTERY"
) {
new Color(
85,
220,
130
)
}
else if (
kind == "CROWBAR"
) {
new Color(
150,
150,
155
)
}
else if (
kind == "NOTE"
) {
new Color(
235,
235,
210
)
}
else if (
kind == "LAMP"
) {
new Color(
242,
206,
110
)
}
else {
new Color(
120,
200,
220
)
}
g.setColor(base)
if (
kind == "KEY"
) {
g.fillOval(
x,
y + h.toInt / 3,
(
w * 0.36
).toInt,
(
w * 0.36
).toInt
)
g.fillRect(
x +
(
w * 0.23
).toInt,
y +
h.toInt / 3 +
(
w * 0.10
).toInt,
(
w * 0.48
).toInt,
(
w * 0.10
).toInt
)
g.fillRect(
x +
(
w * 0.53
).toInt,
y +
h.toInt / 3 +
(
w * 0.10
).toInt,
(
w * 0.10
).toInt,
(
w * 0.26
).toInt
)
}
else if (
kind == "CARD"
) {
g.fillRoundRect(
x,
y,
w.toInt,
h.toInt,
16,
16
)
}
else if (
kind == "LAMP"
) {
g.fillOval(
x +
(
w * 0.18
).toInt,
y +
(
h * 0.10
).toInt,
(
w * 0.64
).toInt,
(
h * 0.28
).toInt
)
g.fillRect(
x +
(
w * 0.42
).toInt,
y +
(
h * 0.34
).toInt,
(
w * 0.16
).toInt,
(
h * 0.50
).toInt
)
}
else {
g.fillRoundRect(
x +
(
w * 0.18
).toInt,
y,
(
w * 0.64
).toInt,
h.toInt,
12,
12
)
}
}
}
// ---------------------------------------------------------------------------
// WORLD OBJECTS / EFFECTS
// ---------------------------------------------------------------------------
def drawPerspectiveFloor(
g: Graphics2D
): Unit = {
val horizon =
VIEW_H / 2 +
cameraBobY.toInt
g.setStroke(
new BasicStroke(
1.0f
)
)
var i = 1
while (
i <= FLOOR_DETAIL
) {
val t =
i.toDouble /
FLOOR_DETAIL.toDouble
val y =
(
horizon +
Math.pow(
t,
1.65
) *
(
VIEW_H -
horizon
)
).toInt
val alpha =
clamp(
150.0 -
i * 4.5,
28.0,
130.0
).toInt
g.setColor(
new Color(
105,
112,
125,
alpha
)
)
g.drawLine(
0,
y,
GAME_W,
y
)
i += 1
}
var x = 0
while (
x <= GAME_W
) {
val center =
GAME_W /
2.0
val spread =
(
x - center
) /
center
val bottomX =
x
val topX =
(
center +
spread *
46.0
).toInt
g.setColor(
new Color(
110,
118,
128,
32
)
)
g.drawLine(
topX,
horizon,
bottomX,
VIEW_H
)
x += 96
}
}
def drawCeilingDetails(
g: Graphics2D
): Unit = {
val horizon =
VIEW_H / 2 +
cameraBobY.toInt
var i = 0
while (
i < 9
) {
val y =
horizon -
20 -
i * 26
val inset =
i * 22
g.setColor(
new Color(
26,
28,
35,
95
)
)
g.fillRect(
inset,
Math.max(
0,
y
),
GAME_W -
inset * 2,
4
)
i += 1
}
}
def drawAmbientVignette(
g: Graphics2D
): Unit = {
val a =
clamp(
55.0 +
fear * 0.65 +
(
100.0 -
battery
) *
0.12,
55.0,
150.0
).toInt
g.setColor(
new Color(
0,
0,
0,
a
)
)
g.fillRect(
0,
0,
GAME_W,
22
)
g.fillRect(
0,
VIEW_H - 22,
GAME_W,
22
)
g.fillRect(
0,
0,
30,
VIEW_H
)
g.fillRect(
GAME_W - 30,
0,
30,
VIEW_H
)
}
def drawInteractPrompt(
g: Graphics2D
): Unit = {
if (
state != "PLAYING" ||
paused ||
showHelp ||
showBlueprint
) {
return
}
var nearText = ""
if (
!keyFound &&
dist(
px,
py,
keyX,
keyY
) < 1.45
) {
nearText =
"E PICK UP BRASS KEY"
}
else if (
!crowbarFound &&
kitchenDoorOpen &&
dist(
px,
py,
crowbarX,
crowbarY
) < 1.45
) {
nearText =
"E TAKE CROWBAR"
}
else if (
!fuseFound &&
crowbarFound &&
dist(
px,
py,
fuseX,
fuseY
) < 1.45
) {
nearText =
"E TAKE EMERGENCY FUSE"
}
else if (
!cardFound &&
powerOn &&
dist(
px,
py,
cardX,
cardY
) < 1.45
) {
nearText =
"E TAKE ACCESS CARD"
}
else if (
!noteFound &&
powerOn &&
dist(
px,
py,
noteX,
noteY
) < 1.45
) {
nearText =
"E READ SECURITY NOTE"
}
else if (
(
!codeA &&
dist(
px,
py,
codeAX,
codeAY
) < 1.45
) ||
(
!codeB &&
dist(
px,
py,
codeBX,
codeBY
) < 1.45
)
) {
nearText =
"E SEARCH CODE FRAGMENT"
}
else if (
!handleFound &&
codeSolved &&
cardFound &&
dist(
px,
py,
handleX,
handleY
) < 1.45
) {
nearText =
"E TAKE EXIT HANDLE"
}
else if (
dist(
px,
py,
generatorX,
generatorY
) < 1.45 &&
!powerOn
) {
nearText =
"E USE GENERATOR"
}
else if (
dist(
px,
py,
securityX,
securityY
) < 1.45 &&
!codeSolved
) {
nearText =
"E USE SECURITY TERMINAL"
}
else if (
dist(
px,
py,
exitX,
exitY
) < 1.55
) {
nearText =
"E CHECK FRONT GATE"
}
if (
nearText != ""
) {
val w = 360
val x =
(
GAME_W - w
) / 2
val y =
VIEW_H - 118
g.setColor(
new Color(
8,
10,
14,
225
)
)
g.fillRoundRect(
x,
y,
w,
42,
14,
14
)
g.setColor(
new Color(
230,
235,
242
)
)
g.drawRoundRect(
x,
y,
w,
42,
14,
14
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
17
)
)
g.drawString(
nearText,
x + 28,
y + 27
)
}
}
def drawWorldProps(
g: Graphics2D
): Unit = {
var i = 0
while (
i < propX.length
) {
if (
propX(i) >= 0.0
) {
drawBillboard(
g,
propX(i),
propY(i),
propKind(i),
0.95,
false
)
}
i += 1
}
if (!keyFound) {
drawBillboard(
g,
keyX,
keyY,
"KEY",
0.65,
true
)
}
if (
!crowbarFound &&
kitchenDoorOpen
) {
drawBillboard(
g,
crowbarX,
crowbarY,
"CROWBAR",
0.70,
true
)
}
if (
!fuseFound &&
crowbarFound
) {
drawBillboard(
g,
fuseX,
fuseY,
"FUSE",
0.62,
true
)
}
if (
!cardFound &&
powerOn
) {
drawBillboard(
g,
cardX,
cardY,
"CARD",
0.70,
true
)
}
if (
!noteFound &&
powerOn
) {
drawBillboard(
g,
noteX,
noteY,
"NOTE",
0.62,
true
)
}
if (
!codeA &&
powerOn
) {
drawBillboard(
g,
codeAX,
codeAY,
"CODEA",
0.60,
true
)
}
if (
!codeB &&
powerOn
) {
drawBillboard(
g,
codeBX,
codeBY,
"CODEB",
0.60,
true
)
}
if (
!handleFound &&
codeSolved &&
cardFound
) {
drawBillboard(
g,
handleX,
handleY,
"HANDLE",
0.68,
true
)
}
if (!batteryFound) {
drawBillboard(
g,
batteryX,
batteryY,
"BATTERY",
0.65,
true
)
}
drawBillboard(
g,
ex,
ey,
"ENEMY",
1.85,
false
)
var li = 0
while (
li < lampX.length
) {
drawBillboard(
g,
lampX(li),
lampY(li),
lampKind(li),
0.55,
true
)
li += 1
}
}
def drawRain(
g: Graphics2D
): Unit = {
if (
difficulty == "EASY"
) {
return
}
var i = 0
while (
i < rainX.length
) {
rainY(i) +=
rainLen(i)
rainX(i) -=
2.2
if (
rainY(i) > VIEW_H ||
rainX(i) < 0
) {
rainY(i) =
-20.0
rainX(i) =
Math.random() *
GAME_W
}
g.setColor(
new Color(
155,
180,
210,
85
)
)
g.drawLine(
rainX(i).toInt,
rainY(i).toInt,
(
rainX(i) -
6.0
).toInt,
(
rainY(i) +
18.0
).toInt
)
i += 1
}
}
def drawFlashlightOverlay(
g: Graphics2D
): Unit = {
if (
!flashlightOn ||
battery <= 0.0
) {
g.setColor(
new Color(
0,
0,
0,
95
)
)
g.fillRect(
0,
0,
GAME_W,
VIEW_H
)
return
}
val cx =
GAME_W / 2
val cy =
VIEW_H / 2
val radius =
(
170 +
Math.sin(
animTick.toDouble *
0.05
) *
10
).toInt
g.setColor(
new Color(
0,
0,
0,
70
)
)
g.fillRect(
0,
0,
GAME_W,
VIEW_H
)
g.setColor(
new Color(
255,
250,
215,
18
)
)
g.fillOval(
cx -
radius * 2,
cy -
radius,
radius * 4,
radius * 2
)
g.setColor(
new Color(
255,
250,
215,
28
)
)
g.fillOval(
cx - radius,
cy - radius / 2,
radius * 2,
radius
)
g.setColor(
new Color(
255,
250,
215,
20
)
)
g.fillOval(
cx -
radius / 2,
cy -
radius / 4,
radius,
radius / 2
)
val cone =
new Polygon()
cone.addPoint(
cx - 110,
cy + 25
)
cone.addPoint(
cx + 110,
cy + 25
)
cone.addPoint(
cx + 270,
VIEW_H
)
cone.addPoint(
cx - 270,
VIEW_H
)
g.setColor(
new Color(
255,
248,
215,
12
)
)
g.fillPolygon(cone)
}
def drawCrosshair(
g: Graphics2D
): Unit = {
val cx =
GAME_W / 2
val cy =
VIEW_H / 2
g.setColor(
new Color(
235,
238,
230,
190
)
)
g.drawLine(
cx - 9,
cy,
cx - 2,
cy
)
g.drawLine(
cx + 2,
cy,
cx + 9,
cy
)
g.drawLine(
cx,
cy - 9,
cx,
cy - 2
)
g.drawLine(
cx,
cy + 2,
cx,
cy + 9
)
}
// ---------------------------------------------------------------------------
// MINIMAP / HUD
// ---------------------------------------------------------------------------
def drawMiniMap(
g: Graphics2D
): Unit = {
if (!showMap) {
return
}
val mw = 300
val mh = 205
val ox = 20
val oy = 18
g.setColor(
new Color(
5,
8,
12,
215
)
)
g.fillRoundRect(
ox,
oy,
mw,
mh,
16,
16
)
val sx = 5.8
val sy = 5.6
var y = 0
while (
y < MAP_H
) {
var x = 0
while (
x < MAP_W
) {
val c =
cellAt(
x,
y
)
if (
c == '#'
) {
g.setColor(
new Color(
95,
100,
112
)
)
}
else if (
c == 'E'
) {
g.setColor(
new Color(
220,
65,
70
)
)
}
else if (
doorAt(x, y) &&
!doorOpenAt(x, y)
) {
g.setColor(
new Color(
177,
122,
66
)
)
}
else {
g.setColor(
new Color(
24,
30,
38
)
)
}
g.fillRect(
(
ox +
x * sx
).toInt,
(
oy +
y * sy
).toInt,
Math.max(
2,
sx.toInt
),
Math.max(
2,
sy.toInt
)
)
x += 1
}
y += 1
}
g.setColor(
new Color(
95,
225,
135
)
)
g.fillOval(
(
ox +
px * sx -
4
).toInt,
(
oy +
py * sy -
4
).toInt,
8,
8
)
g.setColor(
new Color(
235,
70,
80
)
)
g.fillOval(
(
ox +
ex * sx -
4
).toInt,
(
oy +
ey * sy -
4
).toInt,
8,
8
)
g.setColor(
Color.WHITE
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
12
)
)
g.drawString(
"HOUSE MAP",
ox + 12,
oy + 17
)
}
def drawBar(
g: Graphics2D,
x: Int,
y: Int,
w: Int,
value: Double,
max: Double,
label: String,
fill: Color
): Unit = {
g.setColor(
new Color(
12,
15,
20,
220
)
)
g.fillRoundRect(
x,
y,
w,
20,
8,
8
)
g.setColor(fill)
g.fillRoundRect(
x,
y,
(
w *
clamp(
value / max,
0.0,
1.0
)
).toInt,
20,
8,
8
)
g.setColor(
Color.WHITE
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
12
)
)
g.drawString(
label,
x + 8,
y + 15
)
}
def drawHud(
g: Graphics2D
): Unit = {
g.setColor(
new Color(
4,
7,
11,
220
)
)
g.fillRect(
0,
VIEW_H,
GAME_W,
GAME_H -
VIEW_H
)
g.setColor(
Color.WHITE
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
16
)
)
g.drawString(
"ROOM: " +
roomNameAt(
px,
py
),
20,
VIEW_H + 26
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
13
)
)
g.drawString(
"DISCOVERED " +
totalDiscoveries +
"/" +
roomNames.length,
1040,
VIEW_H + 26
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
14
)
)
g.drawString(
"TIME " +
fmtTime(gameTime) +
" SCORE " +
score,
270,
VIEW_H + 25
)
drawBar(
g,
20,
VIEW_H + 40,
220,
health,
100.0,
"HEALTH",
new Color(
220,
82,
90
)
)
drawBar(
g,
260,
VIEW_H + 40,
220,
stamina,
100.0,
"STAMINA",
new Color(
90,
178,
235
)
)
drawBar(
g,
500,
VIEW_H + 40,
220,
battery,
100.0,
"BATTERY",
new Color(
95,
220,
145
)
)
drawBar(
g,
740,
VIEW_H + 40,
220,
fear,
100.0,
"FEAR",
new Color(
185,
105,
225
)
)
g.setColor(
new Color(
225,
232,
240
)
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
13
)
)
g.drawString(
"WASD/ARROWS move | SHIFT sprint | C crouch | mouse look | E interact | F flashlight | H hide | M map | B blueprint | P/ESC pause | F1 help",
20,
VIEW_H + 88
)
g.drawString(
"KEY " +
(
if (keyFound)
"OK"
else
"--"
) +
" CROWBAR " +
(
if (crowbarFound)
"OK"
else
"--"
) +
" FUSE " +
(
if (fuseFound)
"OK"
else
"--"
) +
" POWER " +
(
if (powerOn)
"ON"
else
"OFF"
) +
" CARD " +
(
if (cardFound)
"OK"
else
"--"
),
20,
VIEW_H + 111
)
g.drawString(
"NOTE " +
(
if (noteFound)
"OK"
else
"--"
) +
" CODE " +
(
if (codeSolved)
"OPEN"
else
"LOCKED"
) +
" HANDLE " +
(
if (handleFound)
"OK"
else
"--"
) +
" WATCHER " +
enemyState +
" DIST " +
fmt1(enemyDistance) +
" ROOMS " +
totalDiscoveries +
"/" +
roomNames.length,
590,
VIEW_H + 111
)
g.setColor(
new Color(
255,
235,
185
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
15
)
)
g.drawString(
currentObjective(),
20,
VIEW_H - 18
)
if (
messageTicks > 0
) {
g.setColor(
new Color(
255,
245,
210
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
17
)
)
g.drawString(
message,
20,
VIEW_H - 45
)
}
if (hidden) {
g.setColor(
new Color(
115,
220,
235
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
18
)
)
g.drawString(
"HIDDEN",
1280,
35
)
}
else if (crouched) {
g.setColor(
new Color(
160,
220,
205
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
16
)
)
g.drawString(
"CROUCH",
1280,
35
)
}
if (!flashlightOn) {
g.setColor(
new Color(
245,
180,
95
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
14
)
)
g.drawString(
"FLASHLIGHT OFF",
1280,
60
)
}
}
// ---------------------------------------------------------------------------
// OVERLAYS
// ---------------------------------------------------------------------------
def drawHelp(
g: Graphics2D
): Unit = {
if (!showHelp) {
return
}
g.setColor(
new Color(
5,
8,
12,
240
)
)
g.fillRoundRect(
220,
70,
1000,
555,
24,
24
)
g.setColor(
new Color(
245,
245,
240
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
32
)
)
g.drawString(
"HOW TO PLAY",
560,
115
)
val lines = Array(
"Explore the original Old House and search every room.",
"Find the key -> unlock the service door -> find the crowbar.",
"Use the crowbar to reach the fuse -> restore the generator.",
"Power enables the access card, security note and code fragments.",
"Enter the code at the security terminal -> reach the hidden safe room.",
"Take the exit handle and return to the front gate.",
"The Watcher reacts to sight and noise. Sprinting is loud.",
"Aim the flashlight at the Watcher to stun it for a short time.",
"Hold C to crouch; crouching reduces movement noise and enemy sight range.",
"Use H near furniture marked as a hiding zone.",
"This is an original procedural 3D-style horror game; it does not reproduce a commercial game's map or assets."
)
g.setColor(
new Color(
230,
234,
240
)
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
17
)
)
var i = 0
while (
i < lines.length
) {
g.drawString(
lines(i),
275,
160 +
i * 39
)
i += 1
}
}
def drawBlueprint(
g: Graphics2D
): Unit = {
if (!showBlueprint) {
return
}
g.setColor(
new Color(
4,
6,
10,
245
)
)
g.fillRoundRect(
115,
55,
1210,
625,
22,
22
)
g.setColor(
new Color(
240,
242,
250
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
28
)
)
g.drawString(
"FULL HOUSE BLUEPRINT",
565,
95
)
val sx = 145.0
val sy = 125.0
val cw = 24.0
val ch = 18.0
var y = 0
while (
y < MAP_H
) {
var x = 0
while (
x < MAP_W
) {
val c =
cellAt(
x,
y
)
if (
c == '#'
) {
g.setColor(
new Color(
84,
92,
108
)
)
}
else if (
c == 'E'
) {
g.setColor(
new Color(
220,
70,
78
)
)
}
else if (
doorAt(x, y) &&
!doorOpenAt(x, y)
) {
g.setColor(
new Color(
190,
130,
70
)
)
}
else {
g.setColor(
new Color(
24,
31,
42
)
)
}
g.fillRect(
(
sx +
x * cw
).toInt,
(
sy +
y * ch
).toInt,
cw.toInt - 1,
ch.toInt - 1
)
x += 1
}
y += 1
}
g.setColor(
new Color(
100,
230,
135
)
)
g.fillOval(
(
sx +
px * cw -
5
).toInt,
(
sy +
py * ch -
5
).toInt,
10,
10
)
g.setColor(
new Color(
240,
70,
80
)
)
g.fillOval(
(
sx +
ex * cw -
5
).toInt,
(
sy +
ey * ch -
5
).toInt,
10,
10
)
g.setColor(
Color.WHITE
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
15
)
)
g.drawString(
"Green player | Red Watcher | Brown locked door | Red gate",
165,
655
)
}
def drawPause(
g: Graphics2D
): Unit = {
if (!paused) {
return
}
g.setColor(
new Color(
0,
0,
0,
180
)
)
g.fillRect(
0,
0,
GAME_W,
GAME_H
)
g.setColor(
Color.WHITE
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
54
)
)
g.drawString(
"PAUSED",
590,
300
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
18
)
)
g.drawString(
"Press P or ESC to resume",
555,
340
)
}
def drawScare(
g: Graphics2D
): Unit = {
if (
scareFlash <= 0
) {
return
}
g.setColor(
new Color(
130,
0,
15,
Math.min(
205,
scareFlash * 10
)
)
)
g.fillRect(
0,
0,
GAME_W,
VIEW_H
)
}
// ---------------------------------------------------------------------------
// MENU / END
// ---------------------------------------------------------------------------
def drawMenu(
g: Graphics2D
): Unit = {
g.setColor(
new Color(
3,
5,
8
)
)
g.fillRect(
0,
0,
GAME_W,
GAME_H
)
g.setColor(
new Color(
16,
19,
26
)
)
g.fillRect(
300,
120,
840,
420
)
val roof =
new Polygon()
roof.addPoint(
240,
150
)
roof.addPoint(
720,
60
)
roof.addPoint(
1200,
150
)
g.setColor(
new Color(
9,
11,
16
)
)
g.fillPolygon(roof)
g.setColor(
new Color(
54,
58,
66
)
)
g.fillRoundRect(
630,
300,
180,
240,
18,
18
)
g.setColor(
new Color(
219,
66,
70,
150
)
)
g.fillOval(
705,
380,
30,
30
)
g.setColor(
new Color(
235,
238,
242
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
56
)
)
g.drawString(
"OLD HOUSE // OMEGA V5",
455,
110
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
22
)
)
g.drawString(
"NIGHT WATCH // ADVANCED SOFTWARE 3D HORROR ESCAPE",
505,
150
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
15
)
)
g.setColor(
new Color(
180,
186,
196
)
)
g.drawString(
"Explore every room, solve layered puzzles, manage noise, and survive the Watcher.",
505,
177
)
val labels = Array(
"START NIGHT",
"HOW TO PLAY",
"QUIT"
)
var i = 0
while (
i < labels.length
) {
val r =
new Rectangle(
510,
560 + i * 64,
420,
48
)
g.setColor(
new Color(
22,
26,
34,
240
)
)
g.fillRoundRect(
r.x,
r.y,
r.width,
r.height,
14,
14
)
g.setColor(
new Color(
125,
135,
150
)
)
g.drawRoundRect(
r.x,
r.y,
r.width,
r.height,
14,
14
)
g.setColor(
Color.WHITE
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
19
)
)
g.drawString(
labels(i),
r.x + 145,
r.y + 31
)
i += 1
}
g.setColor(
new Color(
208,
215,
225
)
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
15
)
)
g.drawString(
"Difficulty",
545,
775
)
val diffs = Array(
"EASY",
"NORMAL",
"HARD"
)
var d = 0
while (
d < diffs.length
) {
val rr =
new Rectangle(
680 +
d * 120,
750,
100,
38
)
if (
diffs(d) ==
difficulty
) {
g.setColor(
new Color(
78,
145,
105
)
)
}
else {
g.setColor(
new Color(
40,
46,
58
)
)
}
g.fillRoundRect(
rr.x,
rr.y,
rr.width,
rr.height,
10,
10
)
g.setColor(
Color.WHITE
)
g.drawRoundRect(
rr.x,
rr.y,
rr.width,
rr.height,
10,
10
)
g.drawString(
diffs(d),
rr.x + 22,
rr.y + 25
)
d += 1
}
g.setColor(
new Color(
160,
170,
184
)
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
13
)
)
g.drawString(
"Original advanced software 3D renderer using Java2D / Swing | Mouse look | Dynamic lighting | AI",
360,
820
)
if (showHelp) {
drawHelp(g)
}
}
def drawEnd(
g: Graphics2D,
won: Boolean
): Unit = {
g.setColor(
if (won)
new Color(
5,
28,
18
)
else
new Color(
28,
6,
10
)
)
g.fillRect(
0,
0,
GAME_W,
GAME_H
)
g.setColor(
Color.WHITE
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
54
)
)
g.drawString(
if (won)
"YOU ESCAPED"
else
"THE NIGHT ENDED",
485,
215
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
21
)
)
g.drawString(
"Time: " +
fmtTime(gameTime),
610,
275
)
g.drawString(
"Score: " +
score,
610,
310
)
g.drawString(
"Best: " +
(
if (bestTime == 0.0)
"--"
else
fmtTime(bestTime)
),
610,
345
)
g.drawString(
if (won)
"All objectives completed."
else
"The Watcher reached you.",
535,
390
)
g.setFont(
new Font(
"SansSerif",
Font.PLAIN,
14
)
)
g.drawString(
"Original game design. No commercial game's map or assets are reproduced.",
490,
420
)
g.setFont(
new Font(
"SansSerif",
Font.BOLD,
18
)
)
g.drawString(
"R = restart ENTER = restart M = main menu",
520,
455
)
}
// ---------------------------------------------------------------------------
// EXTRA EFFECTS
// ---------------------------------------------------------------------------
def drawLightBeams(
g: Graphics2D
): Unit = {
if (
!flashlightOn ||
battery <= 0.0 ||
paused
) {
return
}
val cx =
GAME_W / 2 +
cameraBobX.toInt
val cy =
VIEW_H / 2 +
cameraBobY.toInt
val alpha =
(
22.0 +
38.0 *
(
battery /
100.0
) -
flashlightFlicker *
12.0
).toInt
val p =
new Polygon()
p.addPoint(
cx - 82,
cy + 18
)
p.addPoint(
cx + 82,
cy + 18
)
p.addPoint(
cx + 360,
VIEW_H
)
p.addPoint(
cx - 360,
VIEW_H
)
g.setColor(
new Color(
255,
248,
220,
Math.max(
8,
Math.min(
68,
alpha
)
)
)
)
g.fillPolygon(p)
var ring = 0
while (
ring < 4
) {
val w =
160 +
ring * 120
val h =
24 +
ring * 20
g.setColor(
new Color(
255,
250,
230,
Math.max(
3,
alpha /
(ring + 2)
)
)
)
g.drawOval(
cx - w,
cy - h,
w * 2,
h * 2
)
ring += 1
}
}
def drawWallSeams(
g: Graphics2D
): Unit = {
var k = 0
while (
k < RAYS
) {
if (
k % 18 == 0
) {
val d =
zBuffer(k)
if (
d <
MAX_DEPTH - 0.2
) {
val h =
clamp(
VIEW_H.toDouble /
safeDepth(d),
6.0,
VIEW_H.toDouble *
1.5
).toInt
val top =
VIEW_H / 2 -
h / 2
val x =
(
k.toDouble *
GAME_W.toDouble /
RAYS.toDouble
).toInt
g.setColor(
new Color(
5,
7,
10,
60
)
)
g.drawLine(
x,
top,
x,
Math.min(
VIEW_H,
top + h
)
)
}
}
k += 1
}
}
def drawHouseAtmosphere(
g: Graphics2D
): Unit = {
var i = 0
while (
i < dustX.length
) {
val phase =
ambientPulse *
(
0.35 +
(
i % 5
) *
0.03
)
val xx =
(
(
dustX(i) +
Math.sin(
phase + i
) *
18.0
) %
GAME_W +
GAME_W
) %
GAME_W
val yy =
(
(
dustY(i) +
Math.cos(
phase *
0.8 +
i
) *
8.0
) %
VIEW_H +
VIEW_H
) %
VIEW_H
val a =
18 +
(
(
i * 7
) %
20
)
g.setColor(
new Color(
210,
205,
190,
a
)
)
g.fillOval(
xx.toInt,
yy.toInt,
2 +
(
i % 3
),
2 +
(
i % 3
)
)
i += 1
}
}
def drawRoomDecor(
g: Graphics2D
): Unit = {
val baseY =
VIEW_H / 2 +
cameraBobY.toInt
val pulse =
(
Math.sin(
ambientPulse *
0.9
) +
1.0
) *
0.5
if (powerOn) {
var i = 0
while (
i < 6
) {
val x =
130 +
i * 230
g.setColor(
new Color(
248,
205,
105,
(
30 +
22 *
pulse
).toInt
)
)
g.fillOval(
x - 28,
Math.max(
26,
baseY -
210 -
(
i % 2
) *
32
),
56,
26
)
g.setColor(
new Color(
120,
100,
58,
150
)
)
g.fillRect(
x - 4,
Math.max(
48,
baseY -
192 -
(
i % 2
) *
32
),
8,
28
)
i += 1
}
}
var j = 0
while (
j < 5
) {
val x =
170 +
j * 270
val y =
baseY -
120 -
(
j % 2
) *
45
g.setColor(
new Color(
24,
26,
32,
120
)
)
g.fillRect(
x,
y,
110,
7
)
g.fillRect(
x + 8,
y - 36,
7,
36
)
g.fillRect(
x + 102,
y - 28,
7,
28
)
g.setColor(
new Color(
55,
45,
38,
130
)
)
g.drawRect(
x + 18,
y - 27,
70,
18
)
j += 1
}
}
def drawPlayerHands(
g: Graphics2D
): Unit = {
val bob =
Math.sin(
headBob
) *
4.0
val base =
VIEW_H +
8 +
bob.toInt
val cx =
GAME_W / 2
g.setColor(
new Color(
70,
72,
76,
235
)
)
g.fillRoundRect(
cx - 150,
base - 58,
72,
110,
24,
24
)
g.fillRoundRect(
cx + 78,
base - 58,
72,
110,
24,
24
)
g.setColor(
new Color(
150,
144,
132,
230
)
)
g.fillOval(
cx - 132,
base - 18,
36,
50
)
g.fillOval(
cx + 96,
base - 18,
36,
50
)
g.setColor(
new Color(
34,
34,
38,
245
)
)
g.fillRoundRect(
cx - 72,
base - 72,
144,
32,
12,
12
)
g.setColor(
new Color(
220,
210,
180,
200
)
)
g.drawRoundRect(
cx - 72,
base - 72,
144,
32,
12,
12
)
}
def drawInteractionReticle(
g: Graphics2D
): Unit = {
if (
interactionPulse <= 0.0
) {
return
}
val r =
(
18.0 +
interactionPulse
).toInt
g.setColor(
new Color(
245,
215,
125,
Math.min(
180,
(
interactionPulse *
12.0
).toInt
)
)
)
g.drawOval(
GAME_W / 2 - r,
VIEW_H / 2 - r,
r * 2,
r * 2
)
}
def drawThreatEdge(
g: Graphics2D
): Unit = {
if (
enemyState != "CHASE" ||
hidden
) {
return
}
val a =
clamp(
(
9.0 -
enemyDistance
) *
17.0,
18.0,
120.0
).toInt
g.setColor(
new Color(
155,
22,
30,
a
)
)
g.fillRect(
0,
0,
18,
VIEW_H
)
g.fillRect(
GAME_W - 18,
0,
18,
VIEW_H
)
g.fillRect(
0,
0,
GAME_W,
12
)
g.fillRect(
0,
VIEW_H - 12,
GAME_W,
12
)
}
def drawScreenGrain(
g: Graphics2D
): Unit = {
if (
screenGrain <= 0.0
) {
return
}
var i = 0
while (
i < 70
) {
val x =
(
(
i * 137 +
animTick.toInt *
3
) %
GAME_W
)
val y =
(
(
i * 61 +
animTick.toInt *
5
) %
VIEW_H
)
val a =
Math.min(
45,
screenGrain.toInt +
(
i % 12
)
)
g.setColor(
new Color(
220,
220,
225,
a
)
)
g.fillRect(
x,
y,
1,
1
)
i += 1
}
}
def drawPlaying(
g: Graphics2D
): Unit = {
val oldTx =
g.getTransform
g.translate(
(
Math.sin(cameraRoll) *
5.0
).toInt,
(
Math.cos(cameraRoll) *
2.0
).toInt
)
drawWallView(g)
drawWallSeams(g)
drawHouseAtmosphere(g)
drawRoomDecor(g)
drawWorldProps(g)
drawLightBeams(g)
drawFlashlightOverlay(g)
drawRain(g)
drawCrosshair(g)
drawInteractPrompt(g)
drawMiniMap(g)
drawHud(g)
drawPause(g)
drawHelp(g)
drawBlueprint(g)
drawAmbientVignette(g)
drawThreatEdge(g)
drawInteractionReticle(g)
drawScreenGrain(g)
drawPlayerHands(g)
drawScare(g)
g.setTransform(
oldTx
)
}
// ---------------------------------------------------------------------------
// MENU CLICK HELPERS
// ---------------------------------------------------------------------------
def handleMenuClick(
x: Int,
y: Int
): Unit = {
if (
state == "MENU"
) {
if (
y >= 550 &&
y < 610
) {
beginGame()
}
else if (
y >= 614 &&
y < 675
) {
showHelp =
!showHelp
}
else if (
y >= 678 &&
y < 738
) {
frame.dispose()
}
if (
y >= 748 &&
y <= 795
) {
if (
x >= 680 &&
x < 780
) {
setDifficulty(
"EASY"
)
}
else if (
x >= 800 &&
x < 900
) {
setDifficulty(
"NORMAL"
)
}
else if (
x >= 920 &&
x < 1020
) {
setDifficulty(
"HARD"
)
}
}
}
else if (
(
state == "WIN" ||
state == "LOSE"
) &&
y > 420 &&
y < 490
) {
beginGame()
}
}
// ---------------------------------------------------------------------------
// PANEL / INPUT
// ---------------------------------------------------------------------------
val panel =
new JPanel {
setFocusable(true)
override def paintComponent(
graphics: Graphics
): Unit = {
super.paintComponent(
graphics
)
val g =
graphics.asInstanceOf[
Graphics2D
]
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
)
g.setRenderingHint(
RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_SPEED
)
if (
state == "MENU"
) {
drawMenu(g)
}
else if (
state == "PLAYING"
) {
drawPlaying(g)
}
else if (
state == "WIN"
) {
drawEnd(
g,
true
)
}
else if (
state == "LOSE"
) {
drawEnd(
g,
false
)
}
}
addKeyListener(
new KeyAdapter {
override def keyPressed(
e: KeyEvent
): Unit = {
val c =
e.getKeyCode
if (
c >= 0 &&
c < keys.length
) {
keys(c) =
true
}
if (
c == KeyEvent.VK_ESCAPE &&
state == "PLAYING"
) {
paused =
!paused
showHelp =
false
showBlueprint =
false
}
if (
c == KeyEvent.VK_P &&
state == "PLAYING"
) {
paused =
!paused
}
if (
c == KeyEvent.VK_F &&
state == "PLAYING" &&
!paused
) {
flashlightOn =
!flashlightOn
}
if (
c == KeyEvent.VK_H &&
state == "PLAYING" &&
!paused
) {
toggleHide()
}
if (
c == KeyEvent.VK_E &&
state == "PLAYING" &&
!paused
) {
interact()
}
if (
c == KeyEvent.VK_M &&
state == "PLAYING"
) {
showMap =
!showMap
}
if (
c == KeyEvent.VK_B &&
state == "PLAYING"
) {
showBlueprint =
!showBlueprint
}
if (
c == KeyEvent.VK_F1
) {
showHelp =
!showHelp
}
if (
c == KeyEvent.VK_F6
) {
muted =
!muted
}
if (
c == KeyEvent.VK_ENTER &&
state == "MENU"
) {
beginGame()
}
if (
(
c == KeyEvent.VK_R ||
c == KeyEvent.VK_ENTER
) &&
(
state == "WIN" ||
state == "LOSE"
)
) {
beginGame()
}
if (
c == KeyEvent.VK_M &&
(
state == "WIN" ||
state == "LOSE"
)
) {
state =
"MENU"
}
// FIX: repaint() directly
repaint()
}
override def keyReleased(
e: KeyEvent
): Unit = {
val c =
e.getKeyCode
if (
c >= 0 &&
c < keys.length
) {
keys(c) =
false
}
}
}
)
addMouseListener(
new MouseAdapter {
override def mousePressed(
e: MouseEvent
): Unit = {
handleMenuClick(
e.getX,
e.getY
)
mouseLastX =
e.getX
// FIX: mouseDown is var
true
requestFocusInWindow()
repaint()
}
override def mouseReleased(
e: MouseEvent
): Unit = {
// FIX: mouseDown is var
false
}
}
)
addMouseWheelListener(
new MouseWheelListener {
override def mouseWheelMoved(
e: MouseWheelEvent
): Unit = {
if (
state == "PLAYING" &&
!paused &&
!showHelp &&
!showBlueprint
) {
if (
e.getWheelRotation < 0
) {
setMessage(
"Mouse wheel: look sensitivity focus. Use mouse drag for camera.",
70
)
}
else {
setMessage(
"Mouse wheel: use drag to turn. C = crouch.",
70
)
}
interactionPulse =
12.0
}
repaint()
}
}
)
addMouseMotionListener(
new MouseMotionAdapter {
override def mouseDragged(
e: MouseEvent
): Unit = {
if (
state == "PLAYING" &&
!paused &&
!showHelp &&
!showBlueprint
) {
pa =
angleNorm(
pa +
(
e.getX -
mouseLastX
).toDouble *
MOUSE_SENS
)
mouseLastX =
e.getX
}
}
}
)
}
// ---------------------------------------------------------------------------
// PARTICLE INITIALIZATION
// ---------------------------------------------------------------------------
var iRain = 0
while (
iRain < rainX.length
) {
rainX(iRain) =
Math.random() *
GAME_W
rainY(iRain) =
Math.random() *
VIEW_H
rainLen(iRain) =
6.0 +
Math.random() *
10.0
iRain += 1
}
var iDust = 0
while (
iDust < dustX.length
) {
dustX(iDust) =
Math.random() *
GAME_W
dustY(iDust) =
Math.random() *
VIEW_H
iDust += 1
}
// ---------------------------------------------------------------------------
// GAME TIMER
// ---------------------------------------------------------------------------
val timer =
new javax.swing.Timer(
16,
new ActionListener {
override def actionPerformed(
e: ActionEvent
): Unit = {
animTick += 1L
if (
state == "PLAYING" &&
!paused &&
running
) {
gameTime +=
0.016
if (
messageTicks > 0
) {
messageTicks -= 1
}
if (
doorPulse > 0
) {
doorPulse -= 1
}
if (
exitGlow < 30 &&
finalGateOpen
) {
exitGlow += 1
}
if (
interactionPulse > 0.0
) {
interactionPulse -= 1.0
}
ambientPulse +=
0.045
flashlightFlicker =
if (
battery < 12.0 &&
flashlightOn
) {
(
Math.sin(
animTick.toDouble *
0.65
) +
1.0
) *
0.5
}
else {
0.0
}
// FIX:
// Do NOT use local 'sprinting' here.
// Use global sprintingNow.
cameraRoll =
Math.sin(
headBob *
0.45
) *
(
if (sprintingNow)
0.009
else
0.004
)
screenGrain =
clamp(
screenGrain +
(
if (
fear > 65.0
)
0.8
else
-0.45
),
0.0,
18.0
)
if (
Math.random() <
0.0018
) {
lightning = 7
}
if (
lightning > 0
) {
lightning -= 1
}
updatePlayer(
0.016
)
updateEnemy(
0.016
)
if (
scareFlash > 0
) {
scareFlash -= 1
}
if (
enemyState == "CHASE" &&
animTick % 70L == 0L
) {
safeTone()
}
}
panel.repaint()
}
}
)
// ---------------------------------------------------------------------------
// START APPLICATION
// ---------------------------------------------------------------------------
SwingUtilities.invokeLater(
new Runnable {
def run(): Unit = {
frame.setContentPane(
panel
)
frame.setVisible(
true
)
panel.requestFocusInWindow()
timer.start()
}
}
)