destroyulator/game/dev/probe_horror.gd
Monster Robot Party 006c324f1f LANE7: LEVEL 0 gets an entity, a tape, and a loop
THE ENTITY (Entity.gd) borrows from three places and each borrowing does a job:

  SLENDERMAN  it does not move while you look at it, and has closed the distance every
              time you look back. Looking is ALSO what builds the static — so the safe
              option (keep it in view) kills you slowly and the fast option (run) lets
              it catch up. That's the trap, and it's why Slender worked.
  XENOMORPH   it hunts SOUND, and the thing you came here to do is smash furniture.
              Every break and every puff of powder drops a marker it walks toward.
              Playing well is what feeds it. A loud enough noise overrides the freeze,
              because staring is a valid answer to a stalker and not a valid answer to
              something that just heard a sledgehammer hit a filing cabinet.
  BLAIR WITCH the level wraps: walk far enough and you come out of the opposite wall
              into the same room, because every room here IS the same room. A wrap is
              also when it repositions, so "I've been here before" and "it's already
              here" land in the same second.

It can't be fought. Getting caught doesn't kill you — it costs a lungful, a spike of
rage, and the knowledge that it can do that whenever it likes.

Deliberately unresolved design: 2.5 m, faceless, arms past the knees, cranium swept
backwards. Slenderman from the front, something else in profile. A silhouette your brain
finishes beats a monster it recognises. Blair Witch stick totems hang in the maze.

THE TAPE (Dread.gd) — one shader: grain, scanlines, chroma split, a wandering tracking
tear, an edge that breathes when it's near but unseen, and Slender interference driven
straight off the stare. The static IS the read-out; there's no meter for it.

TWO BUGS WORTH THE COMMIT MESSAGE
1. `seen_now` was a pure view-cone test with NO line of sight, so it built static
   through walls — and in a maze that's most of the time.
2. Fixing that exposed the real one: the generated maze was so dense there was no
   sightline longer than a few metres ANYWHERE, which kills the entity outright, since
   a stalker you can never see can never be stared at. Thinned the generator — skip a
   third of the lattice lines, much bigger gaps — which also matches the reference
   photographs far better. They're mostly open floor with occasional slabs, not
   corridors.

dev/probe_horror.gd verifies all of it headlessly, including hunting for a clear
sightline first (otherwise the Slender test silently tests nothing).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:25:28 +10:00

96 lines
3.3 KiB
GDScript

extends SceneTree
## LEVEL 0's horror layer, verified headlessly: does the entity wake only there, does it
## FREEZE while watched and CLOSE while not (the Slender rule), does noise pull it (the
## xenomorph rule), does the loop wrap you, and does all of it shut down on the way out.
func _initialize() -> void:
var main: Node = (load("res://main.tscn") as PackedScene).instantiate()
get_root().add_child(main)
await process_frame
main._load_level("backrooms")
for i in 8:
await physics_frame
var br = main.get("_backrooms")
var ent = br.entity
var player = main.get("_player")
var cam = main.get("_cam")
print("active : ", br.active, " entity awake: ", ent.is_awake())
print("totems hung : ", br._totems.size())
# --- SLENDER RULE: frozen while watched ---
# It's a maze, so a random pair of points usually has a wall between them and the
# line-of-sight test correctly reports "not seen". Hunt for a clear sightline first,
# or this tests nothing.
player.global_position = Vector3(0, 0, 0)
player.rotation.y = 0.0 # facing -Z
var placed := false
for d in [3.0, 4.0, 5.0, 6.0, 8.0, 10.0]:
ent.global_position = Vector3(0, 0, -d)
await physics_frame
await physics_frame
if ent.seen_now:
print("clear sightline at %.0f m" % d)
placed = true
break
if not placed:
print("NO clear sightline found — maze too dense at this seed")
for i in 3:
await physics_frame
var before = ent.global_position
for i in 40:
await physics_frame
var moved_watched = before.distance_to(ent.global_position)
print("watched : moved %.2f m seen=%s static=%.2f" % [
moved_watched, ent.seen_now, ent.static_level])
# --- and closes when you look away ---
player.rotation.y = PI # now facing +Z, away from it
ent.state = Entity.S.STALKING
before = ent.global_position
for i in 40:
await physics_frame
print("looking away : moved %.2f m seen=%s" % [
before.distance_to(ent.global_position), ent.seen_now])
# --- XENOMORPH RULE: it walks toward noise ---
ent.global_position = Vector3(14, 0, 14)
ent.state = Entity.S.STALKING
ent.noise_heat = 0.0
player.rotation.y = 0.0 # look AWAY from it, at -Z
var noise := Vector3(-14, 0, -14)
br.on_noise(noise, 2.0)
var d0 = ent.global_position.distance_to(noise)
for i in 60:
await physics_frame
var d1 = ent.global_position.distance_to(noise)
print("noise at %s : closed %.2f m toward it" % [noise, d0 - d1])
# --- BLAIR WITCH: the loop ---
var loops0 = br.loops
player.global_position = Vector3(0, 0, 18.6) # right at the boundary
for i in 6:
await physics_frame
print("wrap : loops %d -> %d player now z=%.1f" % [
loops0, br.loops, player.global_position.z])
# --- the scare costs you something ---
var dust = main.get("_dust")
var rage = main.get("_rage")
var lungs0 = dust.inhaled
var rage0 = rage.value
ent._cd = 0.0
ent._catch()
await process_frame
print("caught : lungs %.2f -> %.2f rage %.2f -> %.2f touched=%d" % [
lungs0, dust.inhaled, rage0, rage.value, br.caught])
# --- and none of it follows you to the office ---
main._load_level("scranton")
for i in 6:
await physics_frame
print("back at office: horror active=%s entity awake=%s dust active=%s" % [
br.active, ent.is_awake(), dust.active])
quit()