From a806272a7f4b72c6386c653c7c5fe4fe8a271b19 Mon Sep 17 00:00:00 2001 From: Monster Robot Party Date: Fri, 31 Jul 2026 20:07:44 +1000 Subject: [PATCH] LANE7: there are two of them, and the room changes one thing every loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE OTHER ONE (Entity.Kind.MIRROR) After three loops a second one turns up, and it shares the first one's silhouette on purpose — same height, same suit, same dark — so at range you cannot tell which you are looking at. Its rules are the hunter's rules INVERTED: hunter the other one while watched frozen IT COMES while unwatched it closes inert noise hunts it deaf correct play keep it in view LOOK AWAY Every instinct the first one trains into you is the way the second one kills you, and that is the entire design. The tells are close-range only and all wrong in the same direction: head thrown back so it stares at the ceiling instead of you, arms raised, and feet that never reach the carpet (Backrooms floats it 14 cm). It also makes no static and doesn't breathe, so the only warning the tape gives for it is the edge closing in — even while you are looking straight at it. ONE THING PER LOOP (Backrooms._disturb) Exactly one change per wrap. Never two. That constraint IS the effect: change nothing and a loop is just a teleport, change several and it reads as a new room, which is the opposite of what this place is for. One change means you are never sure whether you noticed something or imagined it — and since the room is otherwise identical, when you ARE sure it is worse. Escalating: early loops shift a prop half a metre, turn one, or kill a bank of lights. Later ones hang a totem that wasn't there, permanently reveal an object nobody dusted, or put the hunter exactly where you were standing a moment ago. The shift is half a metre, not one — a metre is enough to shove a neighbouring prop and read as TWO changes, and it's too obvious anyway. Half is "…was that there?" dev/probe_two.gd verifies the inversion with numbers (unwatched 0.00 m, watched 4.39 m, noise heat 0.00) and that the disturbance fires per loop. Note the probe has to wait out WRAP_COOLDOWN between laps or only the first wrap ever fires. Co-Authored-By: Claude Opus 5 --- LANES/LANE7-cubicle-hell.md | 20 ++++- game/README.md | 28 +++++++ game/assets/store/entity.glb | Bin 106212 -> 106212 bytes game/assets/store/entity2.glb | Bin 0 -> 96944 bytes game/assets/store/entity2.glb.import | 45 ++++++++++ game/dev/DemoDriver.gd | 23 +++-- game/dev/probe_death.gd.uid | 1 + game/dev/probe_two.gd | 77 +++++++++++++++++ game/dev/probe_two.gd.uid | 1 + game/scripts/Backrooms.gd | 121 ++++++++++++++++++++++++++- game/scripts/Entity.gd | 53 ++++++++++-- tools/gen_horror_props.py | 57 ++++++++++++- 12 files changed, 405 insertions(+), 21 deletions(-) create mode 100644 game/assets/store/entity2.glb create mode 100644 game/assets/store/entity2.glb.import create mode 100644 game/dev/probe_death.gd.uid create mode 100644 game/dev/probe_two.gd create mode 100644 game/dev/probe_two.gd.uid diff --git a/LANES/LANE7-cubicle-hell.md b/LANES/LANE7-cubicle-hell.md index bdacccd..faa55cf 100644 --- a/LANES/LANE7-cubicle-hell.md +++ b/LANES/LANE7-cubicle-hell.md @@ -243,6 +243,20 @@ xenomorph sprays), LOOP (the room simply keeps you at eleven). `Gore.gd` does bl the lens with drips, a wet burst, and the camera going down onto the carpet — slow on purpose, because a jumpscare is a spike but a death should take its time. -Still open: a second entity that behaves differently, and the "did that chair move?" pass -on wrapping — the room is identical on every loop, so *changing exactly one thing* would -be very cheap and very mean. +**THE OTHER ONE** *(built)* — after three loops there are two, and they share a +silhouette. Its rules are the first one's inverted: it advances ONLY while watched, is +inert when you look away, and is completely deaf. Every instinct the hunter trains into +you is how the other one kills you. Tells are close-range only and all wrong the same +way: head thrown back at the ceiling, arms up, feet that never reach the carpet. + +**ONE THING PER LOOP** *(built)* — `Backrooms._disturb()`. Exactly one change per wrap, +never two, because that constraint IS the effect: nothing and a loop is just a teleport, +several and it reads as a new room. One means you're never sure whether you noticed or +imagined it. Escalates from moving a prop half a metre or killing a light bank, to +hanging a totem that wasn't there, to permanently revealing something nobody dusted, to +putting the hunter exactly where you were standing. + +Still open in LEVEL 0: the two entities can't yet be told apart by anything except +getting close, which is correct but means a first-time player has no way to learn the +difference except dying. A tell at medium range — a sound only one of them makes, or one +that never appears in the same frame as the other — would give skill somewhere to live. diff --git a/game/README.md b/game/README.md index 4309efd..7ce41c6 100644 --- a/game/README.md +++ b/game/README.md @@ -91,6 +91,34 @@ settles → spray again, coughing. 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. +**And after three loops there are two of them.** THE OTHER ONE shares the silhouette — +same height, same suit, same dark — so at range you cannot tell which you're looking at. +Its rules are the first one's rules **inverted**: + +| | the hunter | the other one | +|---|---|---| +| while you look at it | frozen | **it comes** | +| while you don't | it closes | inert | +| noise | hunts it | **deaf** | +| correct play | keep it in view, stay quiet | **look away**; noise is irrelevant | + +Every instinct the first one trains into you is the way the second one kills you. The +tells are close-range only, and all wrong in the same direction: its head is thrown back +so it's staring at the ceiling rather than at you, its arms are raised, and its feet +never reach the carpet. + +### The room changes one thing + +Every loop, **exactly one thing** is different. Never two — that constraint is the whole +effect. Change nothing and a loop is just a teleport; change several and it reads as a +new room, which is the opposite of what this place is for. One change means you're never +sure whether you noticed something or imagined it, and since the room is otherwise +identical, when you *are* sure it's worse. + +It escalates: early loops move or turn a prop, or kill a bank of lights. Later ones hang +a totem that wasn't there, permanently reveal an object nobody dusted, or put the entity +where you were standing a moment ago. + ### DREAD, and dying **No health bar.** Nothing else in this game does damage, so hit points would be a diff --git a/game/assets/store/entity.glb b/game/assets/store/entity.glb index 838a165ae9825727109db626b0de91ea9bc34767..a9c40f6fafa540eb12168b4c00337190ea3f2e32 100644 GIT binary patch delta 1048 zcmW-fZAesk7{$*+tGQ~MqtG2#lwe(Zv2?OpjRqaqB)43Zj0(rMn$l4#VzZ*K$QVP* zNcXSv=sIIUK_3!{41%Jxs3=hBLkqF?!JrRC=v*U6C}_|0;hcN!{a=3fxjg6X{n)qn zV{f*M#9V&K^B23bhP zT5K>ekxcV?ZnY_jxQ#2~G$WDKCP3K{xAi*&+wc?qKpBd#6(#f%l$ug|ompe2n?BoZ z&e~4%(f%#fhka;(k7~qT)S{kV4?k)Uk2+I0pKDgmr;WDE=QU`VSDbpgLPC}Y1q6Dc zP;5dTKe34_(t59itzv9P1&S#z3b6&{^m6RfpDQJ-2dl_~0lJ*o&@Dfj{e=&v!n50i)O7HPdV||jQNA^ij$9<%w`y}Lik9YWt_tXOB zVdES98zB0Mr`A_|LA-pW666P2B+2w-Nzse@iF#iOI^4UuC0YP`Q#N zYo(B0C|mV<9f$KszU&eYwOh)hM5^i4Qls8_(uVzHB>73JXyBNS{SvfJ$w_IKQ&fi> zmA|Bw-YVzxL<4DOBn6iw8=W#B=cP@0=snV_nT@2iG?6ybNZL@7ywjUal6a^6psdMA zTrWA|!g^{Q^!DG<<;Y>mM-EkhOe{g3mKwR}$U`C*8*M@{y%>M$2R_!sUXt5;Bw&?e z2U}E5)v^f{Y?U|KEd8Zd+AKd~Gi%n&-A8M8h<*r%b&Q8P6C|TDC;@FzrUfRen_CZ~ zn;OIg_8~|QLe(f~%6OF(xr%GJ!WVAPZ(vxn`$#M6Cu5?Iw2%Fgtp0wvkTA}@Phgyy z)J6YDcfu5F@E@kAX}!V+j%mhOOj8lI1$d*I2l#uHA;Mk_NWdB0eH?cmCqI7QeG)xM p9HPx3j_Y7f4g>PNIXkVsxzsFf)jNRSPJBL%|t_E}};MY}#>_BH#&y1gzVbB|}#o z?45mZw%TObA<-}+iEbW3w=6~zG$xvu$hJ6B6HK5j&O{S2diTe5e|Nw4{jNS=`-ZP3 zgs&!?bN5f~xp94UWT3M@vB+azEuMiG_8aV+N{+e@a^|YWSM9 zqy&y7iodPZsMWeuR0oWc=q@G*T*E$ySt(MLzPkG7$B>o_4tl4s+0 zT;pLlk3r@jLi*14(wi7!9YToQ)HCrLw{R2paf=LV*-5_ZFzXnGNfdVh{HM{A(q((X z6Yz#SA(JGGhj_uW^8yo4ub;C|uuqW*GF9H>4hz#F$+|GW@hkxj^;dx7osGAuQ{u7N TWs$rKJIUk0NaKlFKxW`ID*jF0 diff --git a/game/assets/store/entity2.glb b/game/assets/store/entity2.glb new file mode 100644 index 0000000000000000000000000000000000000000..13840035366974d0335067a72f886b3f9a7ac070 GIT binary patch literal 96944 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%0AZ5zc_VeXPaUb*>V8?)P;D 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 J1+Ut~{{|hdH&y@u literal 0 HcmV?d00001 diff --git a/game/assets/store/entity2.glb.import b/game/assets/store/entity2.glb.import new file mode 100644 index 0000000..0fa804a --- /dev/null +++ b/game/assets/store/entity2.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://oj65sy2sq2j7" +path="res://.godot/imported/entity2.glb-664185e05164cbf13bb141c7656803fa.scn" + +[deps] + +source_file="res://assets/store/entity2.glb" +dest_files=["res://.godot/imported/entity2.glb-664185e05164cbf13bb141c7656803fa.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 43beb15..e374ad9 100644 --- a/game/dev/DemoDriver.gd +++ b/game/dev/DemoDriver.gd @@ -48,14 +48,17 @@ func _find_player(n: Node) -> Player: return null func _build_script() -> void: - # LEVEL 0: stand in front of it, let the dread max out, and die. + # The two of them. Same silhouette at range; the tells are close-range only — the + # other one's head is thrown back, its arms are up, and its feet don't reach. _script = [ {"dur": 1.2, "move_to": null, "look_at": null, "act": "level:backrooms"}, - {"dur": 0.3, "move_to": Vector3(0, 0, 3.0), "look_at": Vector3(0, 1.5, -3.0), - "act": "ent:0,-3"}, - {"dur": 2.6, "move_to": null, "look_at": null, "act": ""}, - {"dur": 0.4, "move_to": null, "look_at": null, "act": "kill"}, - {"dur": 7.0, "move_to": null, "look_at": null, "act": ""}, + {"dur": 0.3, "move_to": Vector3(0, 0, 4.0), "look_at": Vector3(0, 1.6, -4.0), + "act": "ent:0,-4"}, + {"dur": 3.0, "move_to": null, "look_at": null, "act": ""}, + {"dur": 0.3, "move_to": null, "look_at": Vector3(4.0, 1.6, -4.0), "act": "two:4,-4"}, + {"dur": 3.4, "move_to": null, "look_at": null, "act": ""}, + {"dur": 3.0, "move_to": Vector3(2.0, 0, 1.0), "look_at": Vector3(4.0, 1.7, -4.0), + "act": ""}, ] func _motion(verbose := false) -> float: @@ -163,6 +166,14 @@ func _do(act: String) -> void: br.entity.global_position = Vector3(float(p[0]), 0.0, float(p[1])) br.entity.state = Entity.S.STALKING return + if act.begins_with("two:"): + var b4 = _main.get("_backrooms") + if b4 != null and b4.other != null: + var q := act.substr(4).split(",") + b4.other.wake() + b4.other.global_position = Vector3(float(q[0]), 0.14, float(q[1])) + b4.other.state = Entity.S.STALKING + return if act == "kill": var b3 = _main.get("_backrooms") if b3 != null: diff --git a/game/dev/probe_death.gd.uid b/game/dev/probe_death.gd.uid new file mode 100644 index 0000000..436c55a --- /dev/null +++ b/game/dev/probe_death.gd.uid @@ -0,0 +1 @@ +uid://b51u6svomjek2 diff --git a/game/dev/probe_two.gd b/game/dev/probe_two.gd new file mode 100644 index 0000000..86cebc9 --- /dev/null +++ b/game/dev/probe_two.gd @@ -0,0 +1,77 @@ +extends SceneTree + +## The second entity's inverted rules, and that exactly one thing changes per loop. +## Godot --headless --path game --script dev/probe_two.gd + +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 player = main.get("_player") + var other = br.other + + print("at entry: other awake = ", other.is_awake(), " (should be false)") + + # --- loop until it turns up. WRAP_COOLDOWN is 1.2 s, so the probe has to wait it + # --- out or only the first wrap ever fires. + for lap in 6: + var before := _snapshot() + var was: int = br.loops + player.global_position = Vector3(0, 0, 18.6) + for i in 90: + await physics_frame + var after := _snapshot() + print("loop %d -> %d : %d prop(s) moved" % [was, br.loops, _diff(before, after)]) + print("after %d loops: other awake = %s" % [br.loops, other.is_awake()]) + + # --- INVERTED RULE: inert when unwatched --- + player.global_position = Vector3.ZERO + player.rotation.y = 0.0 # facing -Z + other.global_position = Vector3(0, 0.14, 9) # behind us + other.state = Entity.S.STALKING + for i in 4: + await physics_frame + var p0 = other.global_position + for i in 45: + await physics_frame + print("unwatched : moved %.2f m seen=%s (should be ~0)" % [ + p0.distance_to(other.global_position), other.seen_now]) + + # --- and it comes when you DO look --- + player.rotation.y = PI # now facing +Z, straight at it + for i in 4: + await physics_frame + p0 = other.global_position + for i in 45: + await physics_frame + print("watched : moved %.2f m seen=%s (should be large)" % [ + p0.distance_to(other.global_position), other.seen_now]) + + # --- and it is deaf --- + other.noise_heat = 0.0 + br.on_noise(Vector3(-15, 0, -15), 3.0) + print("after a noise: heat=%.2f (should be 0 — it does not care)" % other.noise_heat) + quit() + +func _snapshot() -> Dictionary: + var d := {} + for n in get_nodes_in_group("smashable"): + if is_instance_valid(n) and n is Node3D: + d[n.get_instance_id()] = [(n as Node3D).global_position, + (n as Node3D).rotation.y] + return d + +func _diff(a: Dictionary, b: Dictionary) -> int: + var n := 0 + for k in b: + if not a.has(k): + continue + var pa: Array = a[k] + var pb: Array = b[k] + if (pa[0] as Vector3).distance_to(pb[0]) > 0.25 or absf(pa[1] - pb[1]) > 0.2: + n += 1 + return n diff --git a/game/dev/probe_two.gd.uid b/game/dev/probe_two.gd.uid new file mode 100644 index 0000000..e8c04cf --- /dev/null +++ b/game/dev/probe_two.gd.uid @@ -0,0 +1 @@ +uid://dhifjxyhb351x diff --git a/game/scripts/Backrooms.gd b/game/scripts/Backrooms.gd index b08d188..75a5651 100644 --- a/game/scripts/Backrooms.gd +++ b/game/scripts/Backrooms.gd @@ -49,6 +49,10 @@ const WRAP_COOLDOWN := 1.2 var active := false var entity: Entity var tape: Dread ## the found-footage layer +var other: Entity ## THE OTHER ONE — inverted rules, see Entity.Kind +## Loops before the second one turns up. You get long enough to learn the first set of +## rules properly, and then they stop being true. +const OTHER_AFTER_LOOPS := 3 var rage: Rage var dust: Dust @@ -100,9 +104,16 @@ func setup(player: Node3D, cam: Camera3D, hud: Hud, r: Rage, d: Dust, entity = Entity.new() host.add_child(entity) - entity.setup(player, cam, _half) + entity.setup(player, cam, _half, Entity.Kind.HUNTER) entity.caught_you.connect(_on_caught) + other = Entity.new() + host.add_child(other) + other.setup(player, cam, _half, Entity.Kind.MIRROR) + other.caught_you.connect(_on_caught) + # it hangs — the feet never reach the carpet, which is the close-range tell + other.position.y = 0.14 + ## Called when a level finishes building. `half` is the site's half-extent. func enter(half: float) -> void: _half = half @@ -112,6 +123,7 @@ func enter(half: float) -> void: _wrap_cd = 0.0 entity._bounds = half entity.wake() + other.sleep() dread = 0.0 dying = false death_cause = Death.NONE @@ -124,6 +136,7 @@ func enter(half: float) -> void: func leave() -> void: active = false entity.sleep() + other.sleep() tape.set_active(false) for t in _totems: if is_instance_valid(t): @@ -133,7 +146,7 @@ func leave() -> void: ## 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) + entity.hear(at, loudness) # the other one is deaf; it does not care # ---------------------------------------------------------------- totems ## Hung in doorways you have definitely already walked through. @@ -169,6 +182,11 @@ func _process(dt: float) -> void: 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) + if other.is_awake(): + # it makes no static and it doesn't breathe at you, so the ONLY warning the tape + # gives for the other one is the edge closing in — even while you're staring at it + var d2 := other.global_position.distance_to(_player.global_position) + near = maxf(near, clampf(1.0 - d2 / 13.0, 0.0, 1.0) * 0.8) tape.drive(dt, entity.static_level, near) gore.set_blood(clampf((dread - 0.72) * 2.2, 0.0, 0.55)) @@ -195,6 +213,8 @@ func _tick_dread(dt: float) -> void: gain += DREAD_FROM_STARE * (0.4 + entity.static_level) src = Death.STARE var d := entity.global_position.distance_to(_player.global_position) + if other.is_awake(): + d = minf(d, other.global_position.distance_to(_player.global_position)) if d < 7.0: var g := DREAD_FROM_NEAR * (1.0 - d / 7.0) if g > gain: @@ -297,6 +317,7 @@ func _check_wrap() -> void: entity._reposition(9.0) if entity.state == Entity.S.STALKING and loops >= 2: entity.state = Entity.S.HUNTING # it learns + _disturb() dread = clampf(dread + DREAD_FROM_LOOP, 0.0, 1.0) if loops >= 11 or dread >= 1.0: _die(Death.LOOP) # the room keeps you @@ -306,6 +327,97 @@ func _check_wrap() -> void: if rage != null: rage.provoke(0.035, "the same room") +# ---------------------------------------------------------------- the disturbance +## EXACTLY ONE THING changes per loop. Never two. +## +## That constraint is the whole effect. Change nothing and the loop is just a teleport; +## change several things and it reads as a new room, which is the opposite of what this +## place is for. One change means you are never sure whether you noticed something or +## imagined it — and the room is otherwise pixel-identical, so when you DO notice, you +## are certain, and that's worse. +## +## It escalates: early loops move furniture, later ones start putting things where you +## were standing. +func _disturb() -> void: + var props: Array = [] + for n in _host.get_tree().get_nodes_in_group("smashable"): + if is_instance_valid(n) and n is Node3D: + props.append(n) + + var options: Array = ["shift", "spin", "dark"] + if loops >= 2: + options.append("totem") + if loops >= 3: + options.append("reveal") + if loops >= 5: + options.append("behind") + var pick: String = options[randi() % options.size()] + + match pick: + "shift": + # something is a metre from where it was. Only a metre. + if props.is_empty(): + return + var p: Node3D = props[randi() % props.size()] + # small on purpose: a metre is enough to shove a neighbour and read as two + # changes, and it's also too obvious. Half a metre is "…was that there?" + p.global_position += Vector3(randf_range(-0.55, 0.55), 0.0, + randf_range(-0.55, 0.55)) + "spin": + if props.is_empty(): + return + var q: Node3D = props[randi() % props.size()] + q.rotation.y += randf_range(1.2, 2.6) + "dark": + # a bank of lights simply isn't on any more + var lights: Array = [] + _collect_lights(_host, lights) + if lights.is_empty(): + return + lights.shuffle() + for i in mini(4, lights.size()): + (lights[i] as OmniLight3D).light_energy *= 0.25 + "totem": + # one more of them than there was + if not ResourceLoader.exists(TOTEM): + return + var packed := load(TOTEM) as PackedScene + var n: Node3D = packed.instantiate() + _host.add_child(n) + var fwd := -_player.global_transform.basis.z + fwd.y = 0.0 + n.position = _player.global_position + fwd.normalized() * randf_range(4.0, 8.0) \ + + Vector3(0, 1.30, 0) + n.rotation.y = randf_range(0.0, TAU) + _totems.append(n) + "reveal": + # one object stops being invisible. Permanently. Nobody dusted it. + if dust == null or props.is_empty(): + return + var r: Node3D = props[randi() % props.size()] + if r is Smashable: + dust._coat(r as Smashable) + (r as Smashable).set_meta("dust_until", 0x7FFFFFFF) + "behind": + # it is standing where you were a moment ago + var back := _player.global_transform.basis.z + back.y = 0.0 + entity.global_position = _player.global_position + back.normalized() * 3.2 + entity.state = Entity.S.HUNTING + + # and after enough of them, the other one is here too + if loops >= OTHER_AFTER_LOOPS and not other.is_awake(): + other.wake() + other.position.y = 0.14 + if _hud != null: + _hud.toast("there are two of them.") + +func _collect_lights(n: Node, acc: Array) -> void: + if n is OmniLight3D: + acc.append(n) + for c in n.get_children(): + _collect_lights(c, acc) + func _on_caught() -> void: caught += 1 tape.flash() @@ -352,6 +464,7 @@ func hud_line() -> String: warn = " · it is looking for you" elif entity.state == Entity.S.LUNGE: warn = " · RUN" + var twos := " · TWO" if other.is_awake() else "" var label := "steady" if dread > 0.85: label = "YOU ARE GOING TO DIE HERE" @@ -359,5 +472,5 @@ func hud_line() -> String: label = "get out get out get out" elif dread > 0.34: label = "you are not alone" - return "loops %d · touched %d · signal %d%% · DREAD %d%% (%s)%s" % [ - loops, caught, st, int(round(dread * 100.0)), label, warn] + return "loops %d%s · touched %d · signal %d%% · DREAD %d%% (%s)%s" % [ + loops, twos, caught, st, int(round(dread * 100.0)), label, warn] diff --git a/game/scripts/Entity.gd b/game/scripts/Entity.gd index ce73de9..954a28a 100644 --- a/game/scripts/Entity.gd +++ b/game/scripts/Entity.gd @@ -25,9 +25,24 @@ class_name Entity ## Godot 4.7 GDScript 2.0. const MODEL := "res://assets/store/entity.glb" +const MODEL_OTHER := "res://assets/store/entity2.glb" enum S { DORMANT, STALKING, HUNTING, LUNGE } +## Two of them, and the second one's rules are the FIRST one's rules inverted. +## +## HUNTER freezes while you look at it, closes when you don't, and hunts sound. +## Correct play: keep it in view, stay quiet. +## MIRROR advances ONLY while you look at it, is inert when you don't, and is deaf. +## Correct play: look away. Noise is irrelevant. +## +## They share a silhouette on purpose. At thirty metres in a yellow room you cannot tell +## which one you're looking at, so every sighting is a question, and the answer you +## learned first is the one that kills you. The tells are close-range only: the MIRROR's +## head is thrown back, its arms are up, and its feet never reach the carpet. +enum Kind { HUNTER, MIRROR } +var kind: int = Kind.HUNTER + ## How far it can be seen from; beyond this it simply isn't rendered. const SEE_DIST := 34.0 ## Inside this, a lunge begins. @@ -59,12 +74,15 @@ var _sting: AudioStreamPlayer signal caught_you() ## the jumpscare fires; Backrooms decides the cost -func setup(player: Node3D, cam: Camera3D, half_extent: float) -> void: +func setup(player: Node3D, cam: Camera3D, half_extent: float, + which: int = Kind.HUNTER) -> void: _player = player _cam = cam _bounds = half_extent - if ResourceLoader.exists(MODEL): - var packed := load(MODEL) as PackedScene + kind = which + var path := MODEL if kind == Kind.HUNTER else MODEL_OTHER + if ResourceLoader.exists(path): + var packed := load(path) as PackedScene if packed != null: _mesh = packed.instantiate() add_child(_mesh) @@ -105,8 +123,8 @@ func is_awake() -> bool: ## 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 state == S.DORMANT or kind == Kind.MIRROR: + return # the other one does not care what you break if loudness >= noise_heat * 0.6: noise_at = at noise_heat = clampf(noise_heat + loudness, 0.0, 3.0) @@ -180,8 +198,11 @@ func _physics_process(dt: float) -> void: 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: + # --- STATIC --- + # Only the HUNTER's stare degrades the signal. The MIRROR doesn't stare at you at + # all — it's looking at the ceiling — so its threat is purely that it is closer than + # it was, and it makes no noise on the tape to warn you. + if seen_now and kind == Kind.HUNTER: 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) @@ -192,6 +213,10 @@ func _physics_process(dt: float) -> void: _catch() return + if kind == Kind.MIRROR: + _tick_mirror(dt, dist) + return + match state: S.STALKING: _tick_stalk(dt, dist) @@ -224,6 +249,20 @@ func _tick_hunt(dt: float, dist: float) -> void: elif dist > SEE_DIST * 0.8 and noise_heat < 0.3: state = S.STALKING +## The inversion. It is inert until observed, and then it comes — fast, and in a +## straight line, and it does not stop while you are still looking. Every instinct the +## HUNTER trained into you (keep it in sight, it's safe while watched) is now the way +## you die. +func _tick_mirror(dt: float, dist: float) -> void: + if not seen_now: + return # unwatched, it is furniture + var speed := hunt_speed * 1.15 + if dist < LUNGE_DIST * 1.6: + speed = lunge_speed + _step_toward(_player.global_position, speed * dt) + if dist < 1.5 and _cd <= 0.0: + _catch() + func _tick_lunge(dt: float, dist: float) -> void: _step_toward(_player.global_position, lunge_speed * dt) if dist < 1.5 and _cd <= 0.0: diff --git a/tools/gen_horror_props.py b/tools/gen_horror_props.py index 7472b33..ca426f6 100644 --- a/tools/gen_horror_props.py +++ b/tools/gen_horror_props.py @@ -198,7 +198,62 @@ def p_totem(M): return parts -PROPS = {"entity": p_entity, "totem": p_totem} +def p_entity2(M): + """THE OTHER ONE. Same height, same darkness, same suit — at thirty metres in a + yellow room you cannot tell it from the first one, and that is the entire point, + because its rules are inverted and guessing wrong kills you. + + Up close there are tells, and they're all wrong in the same direction: the head is + tilted fully back so it is looking at the ceiling and not at you, the arms are raised + above it, and the feet hang — Backrooms.gd floats it, so it never touches the carpet.""" + parts = [] + + # legs hang straight and limp; no knee break, no weight on them + for s in (-1, 1): + hip = Vector((s * 0.075, 0.0, 1.16)) + ankle = Vector((s * 0.080, 0.03, 0.10)) + parts.append(assign(span(0.055, 0.036, hip, ankle), M["suit"])) + foot = box((0.085, 0.20, 0.038), loc=(s * 0.080, 0.02, 0.085), + rot=(math.radians(28), 0, 0)) + parts.append(assign(bevel(foot, 0.012, 2), M["suit"])) + + torso = box((0.36, 0.19, 0.80), loc=(0.0, 0.0, 1.56)) + parts.append(assign(bevel(torso, 0.055, 3), M["suit"])) + hips = box((0.24, 0.16, 0.20), loc=(0.0, 0.0, 1.20)) + parts.append(assign(bevel(hips, 0.045, 3), M["suit"])) + for s in (-1, 1): + lap = box((0.09, 0.03, 0.44), loc=(s * 0.078, -0.095, 1.68), + rot=(0, math.radians(s * 5), 0)) + parts.append(assign(lap, M["shirt"])) + + # arms UP, not down — reaching for the ceiling it is already staring at + for s in (-1, 1): + sh = Vector((s * 0.190, 0.0, 1.92)) + elb = Vector((s * 0.290, 0.02, 2.32)) + wr = Vector((s * 0.250, 0.01, 2.76)) + parts.append(assign(span(0.054, 0.042, sh, elb), M["suit"])) + parts.append(assign(span(0.042, 0.032, elb, wr), M["suit"])) + for f in range(4): + a = wr + Vector((s * 0.006 * (f - 1.5), -0.008, 0.015)) + b = a + Vector((s * 0.014 * (f - 1.5), -0.02, 0.19 + 0.02 * abs(f - 1.5))) + parts.append(assign(span(0.010, 0.006, a, b, verts=6), M["skin"])) + parts.append(assign(ball(0.035, loc=tuple(wr), segs=12, rings=8), M["skin"])) + + # head thrown back: the throat is what faces you + neck = span(0.050, 0.058, Vector((0, 0, 1.94)), Vector((0, -0.09, 2.14))) + parts.append(assign(neck, M["skin"])) + skull = ball(0.112, loc=(0.0, -0.12, 2.22), segs=24, rings=16) + skull.scale = (0.84, 1.02, 1.34) + bpy.ops.object.transform_apply(scale=True) + parts.append(assign(skull, M["skin"])) + back = ball(0.090, loc=(0.0, -0.02, 2.30), segs=20, rings=14) + back.scale = (0.76, 0.90, 1.50) + bpy.ops.object.transform_apply(scale=True) + parts.append(assign(back, M["skin"])) + return parts + + +PROPS = {"entity": p_entity, "entity2": p_entity2, "totem": p_totem} def materials():