From 006c324f1f2ae41ce6ca8bdbf42cc9bf1d0b6423 Mon Sep 17 00:00:00 2001 From: Monster Robot Party Date: Fri, 31 Jul 2026 19:25:28 +1000 Subject: [PATCH] LANE7: LEVEL 0 gets an entity, a tape, and a loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- LANES/LANE7-cubicle-hell.md | 28 +++ game/README.md | 27 +++ game/assets/store/entity.glb | Bin 0 -> 106212 bytes game/assets/store/entity.glb.import | 45 +++++ game/assets/store/totem.glb | Bin 0 -> 32208 bytes game/assets/store/totem.glb.import | 45 +++++ game/dev/DemoDriver.gd | 42 ++-- game/dev/probe_horror.gd | 95 +++++++++ game/dev/probe_horror.gd.uid | 1 + game/scripts/Backrooms.gd | 197 ++++++++++++++++++ game/scripts/Backrooms.gd.uid | 1 + game/scripts/Dread.gd | 110 +++++++++++ game/scripts/Dread.gd.uid | 1 + game/scripts/Dust.gd | 3 + game/scripts/Entity.gd | 296 ++++++++++++++++++++++++++++ game/scripts/Entity.gd.uid | 1 + game/scripts/Levels.gd | 13 +- game/scripts/Main.gd | 38 +++- tools/gen_horror_props.py | 289 +++++++++++++++++++++++++++ 19 files changed, 1203 insertions(+), 29 deletions(-) create mode 100644 game/assets/store/entity.glb create mode 100644 game/assets/store/entity.glb.import create mode 100644 game/assets/store/totem.glb create mode 100644 game/assets/store/totem.glb.import create mode 100644 game/dev/probe_horror.gd create mode 100644 game/dev/probe_horror.gd.uid create mode 100644 game/scripts/Backrooms.gd create mode 100644 game/scripts/Backrooms.gd.uid create mode 100644 game/scripts/Dread.gd create mode 100644 game/scripts/Dread.gd.uid create mode 100644 game/scripts/Entity.gd create mode 100644 game/scripts/Entity.gd.uid create mode 100644 tools/gen_horror_props.py diff --git a/LANES/LANE7-cubicle-hell.md b/LANES/LANE7-cubicle-hell.md index 29ba8d0..8bb5acd 100644 --- a/LANES/LANE7-cubicle-hell.md +++ b/LANES/LANE7-cubicle-hell.md @@ -208,3 +208,31 @@ again, coughing. Pairs naturally with TOTAL DESTRUCTION, because the level *is* Still open here: a second gas with a different trade (something that reveals further but costs more lungs), and the entity. There is currently nothing in here with you. + +### LEVEL 0's horror layer *(built)* + +`Entity.gd` · `Dread.gd` · `Backrooms.gd` · `tools/gen_horror_props.py` + +Three borrowings, each doing a different job. **Slenderman**: frozen while watched, +closer every time you look back, and staring is what builds the static — so the safe +play kills you slowly and the fast play lets it catch up. **Xenomorph**: it hunts sound, +and the objective is to make sound, so playing well feeds it. **Blair Witch**: the level +wraps, and a wrap is when it repositions, so "I've been here" and "it's already here" +arrive together. + +The entity is deliberately unresolved — 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. + +Two bugs worth remembering: +- `seen_now` was a pure view-cone test with **no line of sight**, so it built static + through walls. In a maze that's most of the time. +- Then the fix 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 — 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 photos far + better; they're mostly open floor with occasional slabs. + +Still open: it can only touch you, never end a run. A real fail state, a second entity +that behaves differently, and the "did that chair move?" pass on wrapping — the room is +identical, so *changing one thing* on a loop would be very cheap and very mean. diff --git a/game/README.md b/game/README.md index 8993c23..bc60477 100644 --- a/game/README.md +++ b/game/README.md @@ -70,6 +70,33 @@ jolts, the screen blooms, and it feeds the rage meter, because of course it does The intended rhythm is spray → switch to something heavy → break it before the coating settles → spray again, coughing. +### …and there is something in there with you + +`scripts/Entity.gd` borrows from three places, and each borrowing does a different job: + +- **Slenderman** — it does not move while you are looking at it, and it has closed the + distance every time you look back. Looking at it is *also* what builds the **static**, + so the safe option (keep it in view) is the one that kills you slowly and the fast + option (run) is the one that lets it catch up. That's the trap, and it's why Slender + worked. +- **Xenomorph** — it hunts by **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; playing quietly means not playing. A loud enough noise + overrides the freeze entirely. +- **Blair Witch** — walk far enough in any direction and you come out of the opposite + wall into the same room, because every room here *is* the same room. And a wrap is + when the entity gets to reposition, so "I've been here before" and "it's already here" + land in the same second. + +It can't be fought — weapons pass through. The outs are distance, silence, and not +looking, three things the rest of the game actively discourages. Getting caught doesn't +kill you; it costs you a lungful, a spike of rage, and the knowledge that it can do that +whenever it likes. + +`scripts/Dread.gd` is the tape: grain, scanlines, chroma split, a wandering tracking +tear, and Slender's interference driven straight off the entity's stare — the HUD element +that isn't a HUD element. + ## The four modes `scripts/Modes.gd`, cycled with M. diff --git a/game/assets/store/entity.glb b/game/assets/store/entity.glb new file mode 100644 index 0000000000000000000000000000000000000000..838a165ae9825727109db626b0de91ea9bc34767 GIT binary patch literal 106212 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 literal 0 HcmV?d00001 diff --git a/game/assets/store/entity.glb.import b/game/assets/store/entity.glb.import new file mode 100644 index 0000000..c327755 --- /dev/null +++ b/game/assets/store/entity.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://burexbugk1tw" +path="res://.godot/imported/entity.glb-aeb39bf4ffc2f33ed3f1a75f589dae4d.scn" + +[deps] + +source_file="res://assets/store/entity.glb" +dest_files=["res://.godot/imported/entity.glb-aeb39bf4ffc2f33ed3f1a75f589dae4d.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/assets/store/totem.glb b/game/assets/store/totem.glb new file mode 100644 index 0000000000000000000000000000000000000000..8babb6c146bf856793796c14fbfe92c85d3ebc30 GIT binary patch literal 32208 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 XFPwZ8Kj8@%LlB3*il6_>@2me4w)mSQ literal 0 HcmV?d00001 diff --git a/game/assets/store/totem.glb.import b/game/assets/store/totem.glb.import new file mode 100644 index 0000000..718b429 --- /dev/null +++ b/game/assets/store/totem.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://5qpxvj3s5ajo" +path="res://.godot/imported/totem.glb-b8caf92fb06fc9cf491fcba7b13cab07.scn" + +[deps] + +source_file="res://assets/store/totem.glb" +dest_files=["res://.godot/imported/totem.glb-b8caf92fb06fc9cf491fcba7b13cab07.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/game/dev/DemoDriver.gd b/game/dev/DemoDriver.gd index 94f0c1f..df8f674 100644 --- a/game/dev/DemoDriver.gd +++ b/game/dev/DemoDriver.gd @@ -48,25 +48,17 @@ func _find_player(n: Node) -> Player: return null func _build_script() -> void: - # LEVEL 0. Walk in, see nothing, spray, see for a moment, spray again while the haze - # builds. Total Destruction is the right mode here — the level IS a search. + # LEVEL 0, the horror pass. NOTE the level swap is async and enter() repositions the + # entity on wake, so it has to be placed a step LATER or the wake overwrites it. _script = [ - {"dur": 0.4, "move_to": null, "look_at": null, "act": "level:backrooms"}, - {"dur": 0.4, "move_to": null, "look_at": null, "act": "mode:2"}, - {"dur": 2.6, "move_to": Vector3(0, 0, 13.0), "look_at": Vector3(0, 1.3, 2.0), "act": ""}, - {"dur": 1.6, "move_to": Vector3(0, 0, 9.0), "look_at": Vector3(2.0, 1.3, 0.0), "act": ""}, - {"dur": 1.8, "move_to": null, "look_at": null, "act": "puff"}, - {"dur": 1.6, "move_to": null, "look_at": null, "act": "puff"}, - {"dur": 2.2, "move_to": Vector3(2.0, 0, 5.0), "look_at": Vector3(-3.0, 1.2, -1.0), "act": ""}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "puff"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "puff"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "puff"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "puff"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "puff"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "puff"}, - {"dur": 1.4, "move_to": null, "look_at": null, "act": "puff"}, - {"dur": 2.6, "move_to": Vector3(-2.0, 0, 2.0), "look_at": Vector3(-6.0, 1.2, -4.0), "act": ""}, - {"dur": 3.0, "move_to": null, "look_at": null, "act": ""}, + {"dur": 1.2, "move_to": null, "look_at": null, "act": "level:backrooms"}, + {"dur": 0.3, "move_to": Vector3(0, 0, 4.0), "look_at": Vector3(0, 1.5, -4.0), + "act": "ent:0,-4"}, + {"dur": 2.4, "move_to": null, "look_at": null, "act": ""}, + {"dur": 2.4, "move_to": Vector3(0, 0, 1.5), "look_at": Vector3(0, 1.6, -4.0), "act": ""}, + {"dur": 1.6, "move_to": null, "look_at": null, "act": ""}, + {"dur": 2.6, "move_to": Vector3(0, 0, 18.4), "look_at": Vector3(0, 1.3, 19.0), "act": ""}, + {"dur": 2.4, "move_to": null, "look_at": Vector3(3.0, 1.3, -8.0), "act": ""}, ] func _motion(verbose := false) -> float: @@ -166,6 +158,20 @@ func _do(act: String) -> void: if md != null: md.set_mode(int(act.substr(5))) return + if act.begins_with("ent:"): + # park the entity somewhere specific so the tour can look at it on cue + var br = _main.get("_backrooms") + if br != null and br.entity != null: + var p := act.substr(4).split(",") + br.entity.global_position = Vector3(float(p[0]), 0.0, float(p[1])) + br.entity.state = Entity.S.STALKING + return + if act == "scare": + var br2 = _main.get("_backrooms") + if br2 != null and br2.entity != null: + br2.entity._cd = 0.0 + br2.entity._catch() + return if act == "puff": var dd = _main.get("_dust") if dd != null: diff --git a/game/dev/probe_horror.gd b/game/dev/probe_horror.gd new file mode 100644 index 0000000..1d3ef4b --- /dev/null +++ b/game/dev/probe_horror.gd @@ -0,0 +1,95 @@ +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() diff --git a/game/dev/probe_horror.gd.uid b/game/dev/probe_horror.gd.uid new file mode 100644 index 0000000..bf178b4 --- /dev/null +++ b/game/dev/probe_horror.gd.uid @@ -0,0 +1 @@ +uid://buns5cohy373i diff --git a/game/scripts/Backrooms.gd b/game/scripts/Backrooms.gd new file mode 100644 index 0000000..cc04515 --- /dev/null +++ b/game/scripts/Backrooms.gd @@ -0,0 +1,197 @@ +extends Node +class_name Backrooms + +## LEVEL 0's horror layer: the entity, the loop, the totems, and the scares. +## +## THE LOOP is the Blair Witch bit and the most important part. Walk far enough in any +## direction and you come out of the opposite wall — into the same room, because the maze +## is generated from a fixed seed and every part of it looks like every other part. You +## get a lurch, a tear on the tape, and the growing certainty that you have been here. +## +## Crucially, a wrap is also when the entity gets to reposition. So "I've been here +## before" and "it's already here" arrive in the same second, which is a far better +## scare than either on its own. +## +## THE NOISE RULE ties the horror to the game: the entity hunts sound, and the objective +## is to break things. Playing well is what feeds it. Playing quietly means not playing. +## +## Godot 4.7 GDScript 2.0. + +const TOTEM := "res://assets/store/totem.glb" + +## How close to the outer wall triggers a wrap. +const WRAP_MARGIN := 1.4 +## Seconds after a wrap before another can fire (stops thrashing on a corner). +const WRAP_COOLDOWN := 1.2 + +var active := false +var entity: Entity +var dread: Dread +var rage: Rage +var dust: Dust + +var loops := 0 ## how many times the room has come back around +var caught := 0 + +var _player: Node3D +var _cam: Camera3D +var _hud: Hud +var _host: Node3D +var _half := 19.0 +var _wrap_cd := 0.0 +var _totems: Array[Node3D] = [] +var _next_whisper := 0.0 + +## Lines that fire on a wrap. Deliberately flat — the room is doing the work. +const WRAP_LINES := [ + "…you've been here.", + "this is the same room.", + "you turned left. you have always turned left.", + "the carpet has the same stain.", + "you are further in than you were.", + "it was behind you a moment ago.", +] + +func setup(player: Node3D, cam: Camera3D, hud: Hud, r: Rage, d: Dust, + host: Node3D) -> void: + _player = player + _cam = cam + _hud = hud + rage = r + dust = d + _host = host + + dread = Dread.new() + dread.setup() + host.add_child(dread) + + entity = Entity.new() + host.add_child(entity) + entity.setup(player, cam, _half) + entity.caught_you.connect(_on_caught) + +## Called when a level finishes building. `half` is the site's half-extent. +func enter(half: float) -> void: + _half = half + active = true + loops = 0 + caught = 0 + _wrap_cd = 0.0 + entity._bounds = half + entity.wake() + dread.set_active(true) + _scatter_totems() + if _hud != null: + _hud.toast("there is no exit. there is no one else here.") + +func leave() -> void: + active = false + entity.sleep() + dread.set_active(false) + for t in _totems: + if is_instance_valid(t): + t.queue_free() + _totems.clear() + +## Every destroyed object and every puff of powder is a dinner bell. +func on_noise(at: Vector3, loudness: float) -> void: + if active: + entity.hear(at, loudness) + +# ---------------------------------------------------------------- totems +## Hung in doorways you have definitely already walked through. +func _scatter_totems() -> void: + for t in _totems: + if is_instance_valid(t): + t.queue_free() + _totems.clear() + if not ResourceLoader.exists(TOTEM): + return + var packed := load(TOTEM) as PackedScene + for i in 7: + var n: Node3D = packed.instantiate() + _host.add_child(n) + n.position = Vector3(randf_range(-_half + 3.0, _half - 3.0), 1.30, + randf_range(-_half + 3.0, _half - 3.0)) + n.rotation.y = randf_range(0.0, TAU) + _totems.append(n) + +# ---------------------------------------------------------------- per-frame +func _process(dt: float) -> void: + if not active or _player == null: + return + _wrap_cd = maxf(0.0, _wrap_cd - dt) + _check_wrap() + + # how near it is while UNSEEN — the vignette breathing is the only tell you get + var near := 0.0 + if entity.is_awake() and not entity.seen_now: + var d := entity.global_position.distance_to(_player.global_position) + near = clampf(1.0 - d / 16.0, 0.0, 1.0) + dread.drive(dt, entity.static_level, near) + + # it whispers when it's close and behind you + _next_whisper -= dt + if near > 0.55 and _next_whisper <= 0.0: + _next_whisper = randf_range(5.0, 11.0) + if _hud != null: + _hud.toast("something is breathing") + if rage != null: + rage.provoke(0.03, "something is breathing") + +## Cross the boundary and you come out the far side — into a room that is identical, +## because every room here is identical. The entity repositions on the same frame. +func _check_wrap() -> void: + if _wrap_cd > 0.0: + return + var p := _player.global_position + var lim := _half - WRAP_MARGIN + var wrapped := false + if absf(p.x) > lim: + p.x = -signf(p.x) * (lim - 1.2) + wrapped = true + if absf(p.z) > lim: + p.z = -signf(p.z) * (lim - 1.2) + wrapped = true + if not wrapped: + return + _wrap_cd = WRAP_COOLDOWN + loops += 1 + _player.global_position = p + if _player is CharacterBody3D: + (_player as CharacterBody3D).velocity = Vector3.ZERO + dread.flash() + # the good bit: it is somewhere new, and you have no idea where + entity._reposition(9.0) + if entity.state == Entity.S.STALKING and loops >= 2: + entity.state = Entity.S.HUNTING # it learns + if _hud != null: + _hud.toast(WRAP_LINES[loops % WRAP_LINES.size()]) + if rage != null: + rage.provoke(0.035, "the same room") + +func _on_caught() -> void: + caught += 1 + dread.flash() + if _cam != null: + _cam.rotation.x += randf_range(-0.22, 0.22) + _cam.rotation.z += randf_range(-0.2, 0.2) + # it doesn't kill you. It costs you: a lungful, a spike of rage, and the certainty + # that it can do that whenever it likes. + if dust != null: + dust.inhaled = clampf(dust.inhaled + 0.30, 0.0, 1.0) + if rage != null: + rage.provoke(0.16, "IT TOUCHED YOU") + if _hud != null: + _hud.toast("IT TOUCHED YOU") + +func hud_line() -> String: + if not active: + return "" + var st := int(round(entity.static_level * 100.0)) + var warn := "" + if entity.state == Entity.S.HUNTING: + warn = " · it is looking for you" + elif entity.state == Entity.S.LUNGE: + warn = " · RUN" + return "loops %d · touched %d · signal %d%%%s" % [loops, caught, st, warn] diff --git a/game/scripts/Backrooms.gd.uid b/game/scripts/Backrooms.gd.uid new file mode 100644 index 0000000..e068e49 --- /dev/null +++ b/game/scripts/Backrooms.gd.uid @@ -0,0 +1 @@ +uid://qknlqihf742x diff --git a/game/scripts/Dread.gd b/game/scripts/Dread.gd new file mode 100644 index 0000000..654a2e5 --- /dev/null +++ b/game/scripts/Dread.gd @@ -0,0 +1,110 @@ +extends CanvasLayer +class_name Dread + +## The camera you are apparently holding. +## +## One full-screen shader doing four jobs, because they're all the same job — making the +## image feel like a recording of something rather than a view of it: +## +## VHS ......... grain, scanlines, a chroma split, and a tracking tear that wanders. +## Found-footage grammar. It also quietly hides the fact that the +## geometry is very simple. +## STATIC ...... Slender's interference. Driven by Entity.static_level, so it is a +## read-out of how long you've been looking at it — the HUD element that +## isn't a HUD element. +## SCARE ....... a white-hot bloom and a violent tear on the frame it takes you. +## PULSE ....... a slow breathing darkness at the edges when it's near but unseen, +## which is the only warning you get that it's behind you. +## +## Godot 4.7 GDScript 2.0. + +const SHADER := """ +shader_type canvas_item; +uniform float t = 0.0; +uniform float grain : hint_range(0.0, 1.0) = 0.35; +uniform float static_lv : hint_range(0.0, 1.0) = 0.0; +uniform float scare : hint_range(0.0, 1.0) = 0.0; +uniform float near : hint_range(0.0, 1.0) = 0.0; +uniform sampler2D screen_tex : hint_screen_texture, filter_linear; + +float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } + +void fragment() { + vec2 uv = SCREEN_UV; + + // tracking tear: a band that slides down the frame and drags the image sideways + float band = fract(uv.y * 1.4 - t * 0.11); + float tear = smoothstep(0.982, 1.0, band) * (0.55 + static_lv + scare * 4.0); + uv.x += tear * (hash(vec2(floor(uv.y * 200.0), floor(t * 12.0))) - 0.5) * 0.06; + + // static shoves the image around in blocks — legibility falls apart as it stares + if (static_lv > 0.02) { + vec2 blk = floor(uv * vec2(48.0, 90.0)); + float j = hash(blk + floor(t * 20.0)); + uv += (vec2(hash(blk.yx), j) - 0.5) * static_lv * 0.05 * step(0.72, j); + } + + // chroma split, widening with dread + float split = (0.0016 + static_lv * 0.010 + scare * 0.020); + vec3 col; + col.r = texture(screen_tex, uv + vec2(split, 0.0)).r; + col.g = texture(screen_tex, uv).g; + col.b = texture(screen_tex, uv - vec2(split, 0.0)).b; + + // grain + scanlines + float g = hash(uv * vec2(1024.0, 768.0) + fract(t) * 91.0); + col += (g - 0.5) * (grain * 0.13 + static_lv * 0.34); + col *= 0.90 + 0.10 * sin(SCREEN_UV.y * 900.0); + + // the white blocks of real interference + col = mix(col, vec3(g), static_lv * static_lv * 0.34); + + // edges close in when it's near but out of sight — your only warning + vec2 d = (SCREEN_UV - vec2(0.5)) * vec2(1.0, 0.62); + float r = length(d) * 2.0; + float breathe = 0.5 + 0.5 * sin(t * 2.1); + col *= 1.0 - smoothstep(0.42, 1.25, r) * (near * (0.45 + 0.25 * breathe)); + + // the frame it takes you + col = mix(col, vec3(1.0), scare * 0.62); + + COLOR = vec4(col, 1.0); +} +""" + +var _rect: ColorRect +var _t := 0.0 +var _scare := 0.0 +var _face: TextureRect +var _face_left := 0.0 + +func setup() -> void: + layer = 30 # above everything, including the rage vignette + _rect = ColorRect.new() + _rect.set_anchors_preset(Control.PRESET_FULL_RECT) + _rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + var sh := Shader.new() + sh.code = SHADER + var m := ShaderMaterial.new() + m.shader = sh + _rect.material = m + add_child(_rect) + visible = false + +## Driven every frame by Backrooms. +func drive(dt: float, static_lv: float, near: float) -> void: + _t += dt + _scare = maxf(0.0, _scare - dt * 1.7) + var m := _rect.material as ShaderMaterial + m.set_shader_parameter("t", _t) + m.set_shader_parameter("static_lv", static_lv) + m.set_shader_parameter("near", near) + m.set_shader_parameter("scare", _scare) + +func flash() -> void: + _scare = 1.0 + +func set_active(on: bool) -> void: + visible = on + if not on: + _scare = 0.0 diff --git a/game/scripts/Dread.gd.uid b/game/scripts/Dread.gd.uid new file mode 100644 index 0000000..cb6cc5b --- /dev/null +++ b/game/scripts/Dread.gd.uid @@ -0,0 +1 @@ +uid://dp1rttnc5achn diff --git a/game/scripts/Dust.gd b/game/scripts/Dust.gd index 4c9601b..82b95c2 100644 --- a/game/scripts/Dust.gd +++ b/game/scripts/Dust.gd @@ -29,6 +29,7 @@ const COUGH_AT := 0.72 const PUFF_COOLDOWN := 0.55 var rage: Rage +var heard_by: Backrooms ## a puff is a noise, and it hunts sound var active := false ## only true on a site whose spec says invisible var inhaled := 0.0 ## 0..1 @@ -139,6 +140,8 @@ func discharge() -> bool: _puff_snd.pitch_scale = randf_range(0.94, 1.08) _puff_snd.play() _spawn_puff() + if heard_by != null: + heard_by.on_noise(_player.global_position, 0.7) var origin := _cam.global_position var dir := -_cam.global_transform.basis.z diff --git a/game/scripts/Entity.gd b/game/scripts/Entity.gd new file mode 100644 index 0000000..ce73de9 --- /dev/null +++ b/game/scripts/Entity.gd @@ -0,0 +1,296 @@ +extends Node3D +class_name Entity + +## The thing in LEVEL 0. +## +## It borrows from three places, and the borrowings do different jobs: +## +## SLENDERMAN — it does not move while you are looking at it. Look away and it has +## closed the distance. Looking at it is also what builds the STATIC, so +## the safe option (keep it in view) is the one that kills you slowly and +## the fast option (run) is the one that lets it catch up. That's the +## whole trap, and it's why Slender worked. +## +## XENOMORPH — it hunts by SOUND. And the thing you came to LEVEL 0 to do is smash +## furniture, so the game's own objective feeds it. Every break and every +## puff of the extinguisher drops a noise marker it walks toward. +## +## BLAIR WITCH— it uses the loop. When the level wraps you round to the far side, it +## gets to reposition, so "I've been here before" and "it's already here" +## arrive together. +## +## It cannot be fought. Weapons pass through it. The only outs are distance, silence, and +## not looking at it — three things the rest of the game actively discourages. +## +## Godot 4.7 GDScript 2.0. + +const MODEL := "res://assets/store/entity.glb" + +enum S { DORMANT, STALKING, HUNTING, LUNGE } + +## How far it can be seen from; beyond this it simply isn't rendered. +const SEE_DIST := 34.0 +## Inside this, a lunge begins. +const LUNGE_DIST := 3.6 +## Cone (dot product) within which the player counts as LOOKING at it. +const LOOK_DOT := 0.55 + +@export var stalk_speed := 2.5 +@export var hunt_speed := 4.4 +@export var lunge_speed := 9.5 +## Static gained per second while it is on screen, scaled by how close it is. +@export var stare_static := 0.28 +@export var static_decay := 0.22 + +var state: int = S.DORMANT +var static_level := 0.0 ## 0..1 — Slender's interference. At 1.0 it takes you. +var seen_now := false +var noise_at := Vector3.ZERO +var noise_heat := 0.0 + +var _player: Node3D +var _cam: Camera3D +var _bounds := 19.0 +var _mesh: Node3D +var _cd := 0.0 ## seconds until it may act again after a scare +var _think := 0.0 +var _breath: AudioStreamPlayer3D +var _sting: AudioStreamPlayer + +signal caught_you() ## the jumpscare fires; Backrooms decides the cost + +func setup(player: Node3D, cam: Camera3D, half_extent: float) -> void: + _player = player + _cam = cam + _bounds = half_extent + if ResourceLoader.exists(MODEL): + var packed := load(MODEL) as PackedScene + if packed != null: + _mesh = packed.instantiate() + add_child(_mesh) + visible = false + + # it breathes, and you hear it before you see it + _breath = AudioStreamPlayer3D.new() + _breath.stream = _noise_wav(1.6, 70.0, 1.4, true) + _breath.unit_size = 14.0 + _breath.max_db = -2.0 + _breath.autoplay = false + add_child(_breath) + + _sting = AudioStreamPlayer.new() + _sting.stream = _sting_wav() + _sting.volume_db = 2.0 + add_child(_sting) + +## Put it somewhere far from the player and switch it on. +func wake() -> void: + state = S.STALKING + static_level = 0.0 + _cd = 0.0 + visible = true + _reposition(18.0) + if _breath != null and not _breath.playing: + _breath.play() + +func sleep() -> void: + state = S.DORMANT + visible = false + static_level = 0.0 + if _breath != null: + _breath.stop() + +func is_awake() -> bool: + return state != S.DORMANT + +## Something made a noise. The xenomorph half of it walks toward the last loud thing. +func hear(at: Vector3, loudness: float) -> void: + if state == S.DORMANT: + return + if loudness >= noise_heat * 0.6: + noise_at = at + noise_heat = clampf(noise_heat + loudness, 0.0, 3.0) + # A loud enough noise overrides the Slender freeze. Standing still and staring at it + # is a valid answer to a stalker; it is not a valid answer to something that just + # heard you put a sledgehammer through a filing cabinet. The escalation is the point: + # the quiet rules protect you right up until you do the thing the game is about. + if noise_heat > 1.2 and state == S.STALKING: + state = S.HUNTING + +## Teleport somewhere at least `min_dist` away, out of the player's current view if it +## can manage it — being *found* somewhere new is better than being *seen* moving there. +func _reposition(min_dist: float) -> void: + if _player == null: + return + for attempt in 24: + var p := Vector3(randf_range(-_bounds + 2.0, _bounds - 2.0), 0.0, + randf_range(-_bounds + 2.0, _bounds - 2.0)) + if p.distance_to(_player.global_position) < min_dist: + continue + if attempt < 16 and _in_view(p): + continue + global_position = p + return + global_position = Vector3(randf_range(-_bounds, _bounds), 0.0, + randf_range(-_bounds, _bounds)) + +## In the view cone AND actually visible. The line-of-sight test is not optional: this +## is a maze, so most of the time the cone contains a wall, and without the raycast the +## static built while you stared at plasterboard with the thing quietly behind it. +func _in_view(p: Vector3) -> bool: + if _cam == null: + return false + var eye := _cam.global_position + var to := p - eye + if to.length() < 0.01: + return true + if to.normalized().dot(-_cam.global_transform.basis.z) <= LOOK_DOT: + return false + return _clear_line(eye, p + Vector3(0.0, 1.7, 0.0)) + +## True if nothing static blocks the line. Only walls and fittings occlude — the loose +## furniture is chest-high and shouldn't hide a two-and-a-half metre figure. +func _clear_line(from: Vector3, to: Vector3) -> bool: + var space := get_world_3d().direct_space_state + if space == null: + return true + var q := PhysicsRayQueryParameters3D.create(from, to) + q.collide_with_bodies = true + q.collide_with_areas = false + var hit := space.intersect_ray(q) + if hit.is_empty(): + return true + var col = hit.get("collider") + return not (col is StaticBody3D) + +# ---------------------------------------------------------------- per-frame +func _physics_process(dt: float) -> void: + if state == S.DORMANT or _player == null or _cam == null: + return + _cd = maxf(0.0, _cd - dt) + noise_heat = maxf(0.0, noise_heat - dt * 0.35) + + var to_player := _player.global_position - global_position + var dist := to_player.length() + seen_now = _in_view(global_position) and dist < SEE_DIST + + # always face you. It has no other business. + var flat := Vector3(to_player.x, 0.0, to_player.z) + if flat.length() > 0.05: + var want := atan2(-flat.x, -flat.z) + rotation.y = lerp_angle(rotation.y, want, clampf(dt * 6.0, 0.0, 1.0)) + + # --- STATIC: looking at it is what hurts you --- + if seen_now: + var closeness := clampf(1.0 - dist / SEE_DIST, 0.0, 1.0) + static_level = clampf(static_level + stare_static * (0.35 + closeness) * dt, + 0.0, 1.0) + else: + static_level = maxf(0.0, static_level - static_decay * dt) + + if static_level >= 1.0 and _cd <= 0.0: + _catch() + return + + match state: + S.STALKING: + _tick_stalk(dt, dist) + S.HUNTING: + _tick_hunt(dt, dist) + S.LUNGE: + _tick_lunge(dt, dist) + +## Slender's rule: frozen while watched, and closer every time you look back. +func _tick_stalk(dt: float, dist: float) -> void: + _think -= dt + if seen_now: + return # it does not move while you watch + # closes ground, but toward the last noise if there was one — the two rules compose + var goal := _player.global_position + if noise_heat > 0.4: + goal = noise_at + _step_toward(goal, stalk_speed * dt) + if dist < LUNGE_DIST * 2.2 or noise_heat > 1.6: + state = S.HUNTING + +func _tick_hunt(dt: float, dist: float) -> void: + # hunting, it no longer respects being watched. That escalation is the point. + var goal := _player.global_position + if noise_heat > 1.2: + goal = noise_at + _step_toward(goal, hunt_speed * dt) + if dist < LUNGE_DIST: + state = S.LUNGE + elif dist > SEE_DIST * 0.8 and noise_heat < 0.3: + state = S.STALKING + +func _tick_lunge(dt: float, dist: float) -> void: + _step_toward(_player.global_position, lunge_speed * dt) + if dist < 1.5 and _cd <= 0.0: + _catch() + +func _step_toward(goal: Vector3, amount: float) -> void: + var d := goal - global_position + d.y = 0.0 + if d.length() < 0.05: + return + global_position += d.normalized() * amount + +func _catch() -> void: + _cd = 3.0 + static_level = 0.0 + _sting.pitch_scale = randf_range(0.94, 1.06) + _sting.play() + caught_you.emit() + state = S.STALKING + _reposition(16.0) + +# ---------------------------------------------------------------- audio +## A slow rasp for the breathing; `rough` doubles the noise and drops the tone. +func _noise_wav(dur: float, tone: float, decay: float, rough: bool) -> AudioStreamWAV: + var rate := 22050 + var n := int(rate * dur) + var out := PackedFloat32Array() + out.resize(n) + for i in n: + var t := float(i) / float(rate) + var env := exp(-fposmod(t, 0.8) * decay) * 0.8 + var s := sin(TAU * tone * t) * 0.4 * env + if rough: + s += (randf() * 2.0 - 1.0) * 0.35 * env + s += sin(TAU * tone * 0.5 * t) * 0.25 * env + out[i] = clampf(s, -1.0, 1.0) + return _wav(out, rate, true) + +## The jumpscare sting: a downward shriek over a hit of noise. +func _sting_wav() -> AudioStreamWAV: + var rate := 22050 + var dur := 1.15 + var n := int(rate * dur) + var out := PackedFloat32Array() + out.resize(n) + for i in n: + var t := float(i) / float(rate) + var env := exp(-t * 3.2) + var f := 1800.0 - 1500.0 * clampf(t / dur, 0.0, 1.0) + var s := sin(TAU * f * t) * 0.45 * env + s += sin(TAU * f * 1.5 * t) * 0.20 * env + s += (randf() * 2.0 - 1.0) * 0.55 * exp(-t * 11.0) + s += sin(TAU * 46.0 * t) * 0.35 * exp(-t * 2.0) + out[i] = clampf(s, -1.0, 1.0) + return _wav(out, rate, false) + +func _wav(samples: PackedFloat32Array, rate: int, loop: bool) -> AudioStreamWAV: + var w := AudioStreamWAV.new() + w.format = AudioStreamWAV.FORMAT_16_BITS + w.mix_rate = rate + w.stereo = false + var b := PackedByteArray() + b.resize(samples.size() * 2) + for i in samples.size(): + b.encode_s16(i * 2, int(samples[i] * 32767.0)) + w.data = b + if loop: + w.loop_mode = AudioStreamWAV.LOOP_FORWARD + w.loop_end = samples.size() + return w diff --git a/game/scripts/Entity.gd.uid b/game/scripts/Entity.gd.uid new file mode 100644 index 0000000..281c4bf --- /dev/null +++ b/game/scripts/Entity.gd.uid @@ -0,0 +1 @@ +uid://bgi656kbbcclx diff --git a/game/scripts/Levels.gd b/game/scripts/Levels.gd index be79aec..c5bb60f 100644 --- a/game/scripts/Levels.gd +++ b/game/scripts/Levels.gd @@ -463,16 +463,23 @@ static func backrooms() -> Dictionary: var walls: Array = [] # Wall runs on a 4.6 m lattice. Each line gets a few segments with gaps between, so # you can always get through somewhere but never see far. + # Density matters more than it looks. The first pass put a run on every lattice line + # in both axes and the result had NO sightline longer than a few metres — which kills + # the entity outright, because a stalker you can never see can never be stared at. The + # reference photographs are mostly open floor with occasional slabs, so: skip roughly + # a third of the lines, and leave big gaps. var lines := [-13.8, -9.2, -4.6, 0.0, 4.6, 9.2, 13.8] for axis in ["x", "z"]: for at in lines: + if rng.randf() < 0.34: + continue # this line is simply open floor var cursor := -BR_HALF while cursor < BR_HALF - 2.0: - var gap := rng.randf_range(1.6, 6.5) + var gap := rng.randf_range(4.5, 12.0) cursor += gap if cursor >= BR_HALF - 2.0: break - var run := rng.randf_range(3.2, 9.0) + var run := rng.randf_range(3.0, 7.5) var end: float = minf(cursor + run, BR_HALF) if end - cursor > 1.4: walls.append({"a": axis, "at": at, "from": cursor, "to": end}) @@ -480,7 +487,7 @@ static func backrooms() -> Dictionary: # freestanding pillars, because every Backrooms photo has one in the way var slabs: Array = [] - for i in 22: + for i in 16: var px := rng.randf_range(-BR_HALF + 2.0, BR_HALF - 2.0) var pz := rng.randf_range(-BR_HALF + 2.0, BR_HALF - 2.0) slabs.append({"size": Vector3(0.62, BR_H, 0.62), diff --git a/game/scripts/Main.gd b/game/scripts/Main.gd index 71ff45f..29b8205 100644 --- a/game/scripts/Main.gd +++ b/game/scripts/Main.gd @@ -41,6 +41,7 @@ var _ambience: Ambience var _modes: Modes var _gauntlet: Gauntlet var _dust: Dust +var _backrooms: Backrooms func _ready() -> void: randomize() @@ -80,6 +81,8 @@ func _ready() -> void: add_child(_gauntlet) _dust = Dust.new() add_child(_dust) + _backrooms = Backrooms.new() + add_child(_backrooms) # rules layer: the record modes still exist behind M _game_mode = GameMode.new() @@ -94,7 +97,9 @@ func _ready() -> void: _player.gauntlet = _gauntlet _dust.setup(_player, _cam, _rage, _hud) _player.dust = _dust - _dust.adopt(get_tree(), bool(_plan.spec.get("invisible", false))) + _backrooms.setup(_player, _cam, _hud, _rage, _dust, self) + _dust.heard_by = _backrooms + _apply_site_horror() _modes.begin(get_tree().get_nodes_in_group("smashable").size()) ## Every desk is a spreadsheet you have to fill in; the copier is the copier. @@ -461,6 +466,10 @@ func _on_smashed(kind: String, at: Vector3, shards: int) -> void: _shift.on_smashed(int(POINTS.get(kind, 15))) if _modes != null: _modes.on_smashed(int(POINTS.get(kind, 15))) + # LEVEL 0: the entity hunts sound, and breaking things is the objective. Playing well + # is what feeds it. + if _backrooms != null: + _backrooms.on_noise(at, 1.0) if _game_mode != null: _game_mode.on_smashed() # scores the mess during a FindMisfiled round @@ -543,11 +552,7 @@ func _load_level(id: String) -> void: _register_stations() if _ambience != null: _ambience.adopt_lights(_plan) - if _dust != null: - var blind := bool(_plan.spec.get("invisible", false)) - _dust.adopt(get_tree(), blind) - if blind: - _player.select(5) # you will need the extinguisher + _apply_site_horror() if _game_mode != null: _game_mode.begin_free_play(_records) if _shift != null: @@ -599,6 +604,21 @@ func _find_shredder(g: Dictionary) -> Node3D: var res := _glb_piece(P + "shredder.glb", "plastic", Vector2(at.x, at.z), false) return res["piece"] as Node3D +## Turn the invisibility and the horror layer on or off for whatever site we just built. +## Both are LEVEL 0 only, and both have to be explicitly torn down on the way out or they +## leak into an ordinary Tuesday at the Scranton branch. +func _apply_site_horror() -> void: + var blind := bool(_plan.spec.get("invisible", false)) + if _dust != null: + _dust.adopt(get_tree(), blind) + if _backrooms != null: + if blind: + _backrooms.enter(float((_plan.spec["bounds"] as Dictionary)["x1"])) + else: + _backrooms.leave() + if blind and _player != null: + _player.select(5) # you will need the extinguisher + # The Day-0 stress gate: rain N bodies, watch the FPS counter hold. func _rain(n: int) -> void: for i in n: @@ -691,8 +711,10 @@ func _process(_dt: float) -> void: "weapon_blurb": w.blurb if w != null else "", "weapon_stats": ("pow %.1f cd %.2fs reach %.1fm" % [w.power, w.cooldown, w.reach]) if w != null else "", "slot": _player.slot() if _player != null else 0, - "mode_line": _mode_line() + ("\n" + _dust.hud_line() if _dust != null - and _dust.active else ""), + "mode_line": _mode_line() + + ("\n" + _dust.hud_line() if _dust != null and _dust.active else "") + + ("\n" + _backrooms.hud_line() if _backrooms != null + and _backrooms.active else ""), "mode_name": _modes.name_of() if _modes != null else "", "headline": _modes.headline() if _modes != null else "", "keys": KEYS_LINE, diff --git a/tools/gen_horror_props.py b/tools/gen_horror_props.py new file mode 100644 index 0000000..7472b33 --- /dev/null +++ b/tools/gen_horror_props.py @@ -0,0 +1,289 @@ +"""LEVEL 0's inhabitants. + + /Applications/Blender.app/Contents/MacOS/Blender --background \ + --python tools/gen_horror_props.py -- [--render] + +THE ENTITY is deliberately ambiguous. It's 2.5 m, far too thin, has no face, arms that +reach past its knees, and a smooth cranium swept backwards — which reads as Slenderman +from the front and as something else entirely in profile. Keeping it unresolved is the +point: a silhouette your brain finishes is worse than a monster it recognises. + +Origin at floor-centre, facing -Z (so `look_at` and yaw work the obvious way). + +THE TOTEM is the Blair Witch stick figure — twigs lashed into a person — which exists to +be found hanging in a corridor you have definitely already walked down. +""" + +import bpy +import math +import os +import sys +from mathutils import Vector, Matrix + +OUT_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "game", "assets", "store") +PREVIEW_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "previews") +RENDER = "--render" in sys.argv + + +def reset(): + bpy.ops.wm.read_factory_settings(use_empty=True) + + +def mat(name, color, rough=0.6, metal=0.0): + m = bpy.data.materials.new(name) + m.use_nodes = True + b = m.node_tree.nodes["Principled BSDF"] + b.inputs["Base Color"].default_value = (color[0], color[1], color[2], 1.0) + b.inputs["Roughness"].default_value = rough + b.inputs["Metallic"].default_value = metal + return m + + +def assign(o, m): + o.data.materials.clear() + o.data.materials.append(m) + return o + + +def box(size, loc=(0, 0, 0), rot=(0, 0, 0)): + bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc, rotation=rot) + o = bpy.context.active_object + o.scale = (size[0], size[1], size[2]) + bpy.ops.object.transform_apply(scale=True) + return o + + +def cyl(r, depth, loc=(0, 0, 0), rot=(0, 0, 0), verts=14, r2=None): + if r2 is None: + bpy.ops.mesh.primitive_cylinder_add(radius=r, depth=depth, vertices=verts, + location=loc, rotation=rot) + else: + bpy.ops.mesh.primitive_cone_add(radius1=r, radius2=r2, depth=depth, + vertices=verts, location=loc, rotation=rot) + return bpy.context.active_object + + +def ball(r, loc=(0, 0, 0), segs=16, rings=10): + bpy.ops.mesh.primitive_uv_sphere_add(radius=r, segments=segs, ring_count=rings, + location=loc) + return bpy.context.active_object + + +def bevel(o, width=0.006, segments=2): + m = o.modifiers.new("bevel", "BEVEL") + m.width = width + m.segments = segments + m.limit_method = 'ANGLE' + m.angle_limit = math.radians(40) + bpy.context.view_layer.objects.active = o + try: + bpy.ops.object.modifier_apply(modifier=m.name) + except Exception: + o.modifiers.remove(m) + return o + + +def shade_smooth(o, angle=40.0): + bpy.context.view_layer.objects.active = o + o.select_set(True) + try: + bpy.ops.object.shade_auto_smooth(angle=math.radians(angle)) + except Exception: + bpy.ops.object.shade_smooth() + o.select_set(False) + + +def span(r0, r1, p0, p1, verts=8): + p0, p1 = Vector(p0), Vector(p1) + d = p1 - p0 + n = d.length + if n < 1e-6: + return None + o = cyl(r0, n, verts=verts, r2=r1) + quat = Vector((0, 0, 1)).rotation_difference(d.normalized()) + o.matrix_world = Matrix.Translation(p0 + d * 0.5) @ quat.to_matrix().to_4x4() + bpy.ops.object.transform_apply(location=True, rotation=True) + return o + + +# ---------------------------------------------------------------- the entity + +def p_entity(M): + """2.5 m, far too thin, no face. Faces -Y in Blender, which the glTF export turns + into -Z — the direction Godot's look_at treats as forward.""" + parts = [] + H = 2.50 + + # legs: absurdly long, barely tapering, no feet to speak of + for s in (-1, 1): + hip = Vector((s * 0.085, 0.0, 1.18)) + knee = Vector((s * 0.10, 0.02, 0.62)) + ankle = Vector((s * 0.095, -0.01, 0.045)) + parts.append(assign(span(0.062, 0.052, hip, knee), M["suit"])) + parts.append(assign(span(0.052, 0.040, knee, ankle), M["suit"])) + foot = box((0.10, 0.24, 0.045), loc=(s * 0.095, -0.05, 0.022)) + parts.append(assign(bevel(foot, 0.014, 2), M["suit"])) + + # torso: a narrow slab, wider at the shoulders than anything human + torso = box((0.40, 0.20, 0.78), loc=(0.0, 0.0, 1.56)) + parts.append(assign(bevel(torso, 0.055, 3), M["suit"])) + hips = box((0.26, 0.17, 0.22), loc=(0.0, 0.0, 1.20)) + parts.append(assign(bevel(hips, 0.045, 3), M["suit"])) + # lapels, so it reads as a suit and not a mannequin + for s in (-1, 1): + lap = box((0.10, 0.03, 0.46), loc=(s * 0.085, -0.10, 1.68), + rot=(0, math.radians(s * 5), 0)) + parts.append(assign(lap, M["shirt"])) + tie = box((0.05, 0.02, 0.40), loc=(0.0, -0.108, 1.62)) + parts.append(assign(tie, M["tie"])) + + # arms: they reach past the knees. This is the tell. + for s in (-1, 1): + sh = Vector((s * 0.205, 0.0, 1.90)) + elb = Vector((s * 0.255, 0.03, 1.24)) + wr = Vector((s * 0.235, 0.01, 0.58)) + parts.append(assign(span(0.058, 0.044, sh, elb), M["suit"])) + parts.append(assign(span(0.044, 0.034, elb, wr), M["suit"])) + # long pale fingers + for f in range(4): + a = wr + Vector((s * 0.006 * (f - 1.5), -0.012, -0.02)) + b = a + Vector((s * 0.012 * (f - 1.5), -0.03, -0.20 - 0.02 * abs(f - 1.5))) + parts.append(assign(span(0.011, 0.006, a, b, verts=6), M["skin"])) + parts.append(assign(ball(0.038, loc=tuple(wr), segs=12, rings=8), M["skin"])) + + # head: smooth, blank, and swept BACKWARDS — Slenderman from the front, something + # with a cranium from the side. Never resolving which is the whole idea. + neck = span(0.052, 0.060, Vector((0, 0, 1.93)), Vector((0, 0.01, 2.10))) + parts.append(assign(neck, M["skin"])) + skull = ball(0.115, loc=(0.0, 0.03, 2.24), segs=24, rings=16) + skull.scale = (0.86, 1.30, 1.02) + bpy.ops.object.transform_apply(scale=True) + parts.append(assign(skull, M["skin"])) + back = ball(0.095, loc=(0.0, 0.14, 2.27), segs=20, rings=14) + back.scale = (0.78, 1.55, 0.86) + bpy.ops.object.transform_apply(scale=True) + parts.append(assign(back, M["skin"])) + return parts + + +# ---------------------------------------------------------------- the totem + +def p_totem(M): + """Twigs lashed into a person. Hangs from a doorway you have already walked through.""" + parts = [] + # the cord it hangs by + parts.append(assign(span(0.006, 0.006, Vector((0, 0, 1.10)), Vector((0, 0, 0.86))), + M["cord"])) + # body + limbs, all crooked on purpose + parts.append(assign(span(0.015, 0.013, Vector((0, 0, 0.86)), Vector((0.01, 0, 0.40))), + M["twig"])) + parts.append(assign(span(0.013, 0.011, Vector((-0.20, 0.02, 0.78)), + Vector((0.21, -0.02, 0.72))), M["twig"])) + for s in (-1, 1): + parts.append(assign(span(0.012, 0.009, Vector((0.01, 0, 0.42)), + Vector((s * 0.16, 0.03 * s, 0.06))), M["twig"])) + # head: a bundle + for i in range(5): + a = i * 1.1 + parts.append(assign(span(0.008, 0.008, + Vector((math.cos(a) * 0.05, math.sin(a) * 0.04, 0.90)), + Vector((-math.cos(a) * 0.05, -math.sin(a) * 0.04, 0.80))), + M["twig"])) + # lashings + for z in (0.86, 0.75, 0.42): + r = cyl(0.026, 0.014, loc=(0.005, 0, z), verts=10) + parts.append(assign(r, M["cord"])) + return parts + + +PROPS = {"entity": p_entity, "totem": p_totem} + + +def materials(): + return { + # near-black, but not pure — pure black reads as a hole, and a hole is less + # frightening than a thing + "suit": mat("suit", (0.045, 0.045, 0.055), rough=0.72), + "shirt": mat("shirt", (0.80, 0.79, 0.76), rough=0.85), + "tie": mat("tie", (0.10, 0.09, 0.11), rough=0.6), + "skin": mat("skin", (0.90, 0.89, 0.86), rough=0.42), + "twig": mat("twig", (0.32, 0.24, 0.15), rough=0.95), + "cord": mat("cord", (0.58, 0.52, 0.38), rough=1.0), + } + + +def export(objs, name): + objs = [o for o in objs if o is not None] + bpy.ops.object.select_all(action='DESELECT') + for o in objs: + o.select_set(True) + bpy.context.view_layer.objects.active = objs[0] + path = os.path.join(OUT_DIR, name + ".glb") + bpy.ops.export_scene.gltf(filepath=path, export_format='GLB', + use_selection=True, export_apply=True, export_yup=True) + print("[horror] %-12s -> %s" % (name, os.path.basename(path))) + bpy.ops.object.select_all(action='DESELECT') + + +def render_preview(name): + scene = bpy.context.scene + engines = scene.render.bl_rna.properties['engine'].enum_items.keys() + for want in ('BLENDER_EEVEE_NEXT', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'): + if want in engines: + scene.render.engine = want + break + scene.render.resolution_x = 560 + scene.render.resolution_y = 760 + if scene.world is None: + scene.world = bpy.data.worlds.new("preview") + scene.world.use_nodes = True + bg = scene.world.node_tree.nodes.get("Background") + if bg: + bg.inputs[0].default_value = (0.44, 0.42, 0.22, 1.0) # backrooms yellow + lo = Vector((1e9,) * 3) + hi = Vector((-1e9,) * 3) + for o in scene.objects: + if o.type != 'MESH': + continue + for c in o.bound_box: + w = o.matrix_world @ Vector(c) + lo = Vector((min(lo.x, w.x), min(lo.y, w.y), min(lo.z, w.z))) + hi = Vector((max(hi.x, w.x), max(hi.y, w.y), max(hi.z, w.z))) + centre = (lo + hi) * 0.5 + radius = max((hi - lo).length * 0.5, 0.05) + dist = radius * 2.5 + d = Vector((0.42, -0.88, 0.10)).normalized() + bpy.ops.object.camera_add(location=tuple(centre + d * dist)) + cam = bpy.context.active_object + cam.data.lens = 62 + cam.rotation_mode = 'QUATERNION' + cam.rotation_quaternion = (-d).to_track_quat('-Z', 'Y') + scene.camera = cam + for off, e, s in ((Vector((0.7, -1.0, 0.9)), 42.0, 1.4), + (Vector((-1.0, -0.3, 0.2)), 12.0, 2.0)): + bpy.ops.object.light_add(type='AREA', location=tuple(centre + off * dist)) + L = bpy.context.active_object + L.data.energy = e * (dist ** 2) + L.data.size = s * radius + scene.render.filepath = os.path.join(PREVIEW_DIR, name + ".png") + bpy.ops.render.render(write_still=True) + + +def main(): + os.makedirs(OUT_DIR, exist_ok=True) + os.makedirs(PREVIEW_DIR, exist_ok=True) + for name, builder in PROPS.items(): + reset() + M = materials() + parts = builder(M) + for p in parts: + shade_smooth(p, 40.0) + export(parts, name) + if RENDER: + render_preview(name) + print("[horror] done -> %s" % OUT_DIR) + + +main()