LANE7: you can die in LEVEL 0, and how you died decides which death you get

NO HEALTH BAR. Nothing else in this game does damage, so hit points would be a
survival-horror UI bolted onto a demolition game. Instead: one meter, DREAD, fed by
every way this place gets at you — being stared at, something close behind you, each
loop, and your own coughing. It decays when nothing is happening to you, and every loop
makes it grip harder (DREAD_LOOP_GRIP), so the place gets better at this the longer you
stay.

At 100% you die, and WHICH source pushed it over decides the death, which is what keeps
the three memes legible at the one moment it matters:

  STARE  "YOU LOOKED TOO LONG / it was still. it was always still. that was never the
         safe part."                                              (Slenderman)
  REACH  "IT WAS ALREADY IN THE ROOM / you were making so much noise."
         Sprays. This is the gory one.                            (the xenomorph)
  LOOP   "YOU ARE STILL IN THE ROOM / you walked it eleven times. you will walk it
         again." Eleven loops and the room simply keeps you.       (Blair Witch)

Gore.gd is procedural — seeded blobs that swell on the lens and then bleed downward in
streaks, a wet burst at the camera (physical deaths only; being taken by the static
shouldn't spray), the camera going down to land on its side on the carpet, then the
card. The sequence is deliberately SLOW: a jumpscare is a spike, but a death should take
its time, because the seconds spent watching the lens settle are where it lands. R wakes
you up.

Player.set_frozen() so you can't stroll around during your own death.

Renamed Backrooms' VHS layer to `tape` — `dread` is the meter now, and it was clearer
anyway.

dev/probe_death.gd drives all three deaths and the revive.

Note: the "Parameter material is null" warning only appears under the headless dummy
renderer; windowed runs are clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Monster Robot Party 2026-07-31 19:44:26 +10:00
parent 006c324f1f
commit bcd67906a4
9 changed files with 479 additions and 26 deletions

View File

@ -233,6 +233,16 @@ Two bugs worth remembering:
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.
**DREAD and the three deaths** *(built)* — one meter, not a health bar. Nothing else in
this game does damage, so hit points would be a survival-horror UI bolted onto a
demolition game. DREAD is fed by being stared at, by proximity while unseen, by each
loop, and by your own coughing; it decays when nothing is happening to you, and every
loop makes it grip harder. At 100% you die, and whichever source pushed it over decides
which death you get, so the three memes stay legible: STARE (Slender), REACH (gory, the
xenomorph sprays), LOOP (the room simply keeps you at eleven). `Gore.gd` does blood on
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.

View File

@ -89,9 +89,31 @@ settles → spray again, coughing.
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.
looking, three things the rest of the game actively discourages.
### DREAD, and dying
**No health bar.** Nothing else in this game does damage, so hit points would be a
survival-horror UI bolted onto a demolition game. Instead there is one meter — **DREAD**
fed by every way this place gets at you: being stared at, something close behind you,
each loop, and your own coughing. It falls when nothing is happening to you, and every
loop makes it harder to shake.
At 100% you die, and **which** death you get is decided by whichever source pushed it
over — so the three memes stay legible in the moment it matters:
| | how | the card |
|---|---|---|
| **Slenderman** | you looked too long | *YOU LOOKED TOO LONG — it was still. it was always still. that was never the safe part.* |
| **the xenomorph** | it reached you, and it sprays | *IT WAS ALREADY IN THE ROOM — you were making so much noise.* |
| **Blair Witch** | eleven loops of the same room | *YOU ARE STILL IN THE ROOM — you walked it eleven times. you will walk it again.* |
`scripts/Gore.gd` does the rest, procedurally: seeded blobs that swell on the lens and
then bleed downward in streaks, a wet burst at the camera (physical deaths only — being
taken by the static shouldn't spray), and the camera going down to land on its side on
the carpet. The sequence is deliberately **slow** — a jumpscare is a spike, but a death
should take its time, because the seconds you spend watching the lens settle are where it
actually lands. `R` to wake up.
`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

View File

@ -48,17 +48,14 @@ func _find_player(n: Node) -> Player:
return null
func _build_script() -> void:
# 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.
# LEVEL 0: stand in front of it, let the dread max out, and die.
_script = [
{"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": ""},
{"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": ""},
]
func _motion(verbose := false) -> float:
@ -166,6 +163,11 @@ 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 == "kill":
var b3 = _main.get("_backrooms")
if b3 != null:
b3.dread = 0.99
return
if act == "scare":
var br2 = _main.get("_backrooms")
if br2 != null and br2.entity != null:

70
game/dev/probe_death.gd Normal file
View File

@ -0,0 +1,70 @@
extends SceneTree
## All three deaths, and that you can get up again.
## Godot --headless --path game --script dev/probe_death.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")
# --- 1. STARE: park it in view and don't look away ---
print("--- stare ---")
player.global_position = Vector3.ZERO
player.rotation.y = 0.0
var placed := false
for d in [3.0, 4.0, 5.0, 6.0, 8.0]:
br.entity.global_position = Vector3(0, 0, -d)
await physics_frame
await physics_frame
if br.entity.seen_now:
placed = true
break
print(" sightline: ", placed)
var ticks := 0
while not br.dying and ticks < 900:
br.entity.global_position = Vector3(0, 0, -4) # hold it there
await physics_frame
ticks += 1
print(" dying=%s cause=%s after %.1fs dread=%.2f" % [
br.dying, br.death_cause, ticks / 60.0, br.dread])
print(" player frozen: ", player._frozen)
# let the sequence run
for i in 300:
await physics_frame
print(" card shown: ", br._card_shown)
# --- get up ---
br.revive()
main._reset()
for i in 8:
await physics_frame
print(" after revive: dying=%s dread=%.2f frozen=%s" % [
br.dying, br.dread, player._frozen])
# --- 2. REACH: a touch while already terrified ---
print("--- reach ---")
br.dread = 0.80
br.entity._cd = 0.0
br.entity._catch()
await process_frame
print(" dying=%s cause=%s" % [br.dying, br.death_cause])
br.revive()
main._reset()
for i in 6:
await physics_frame
# --- 3. LOOP: walk the same room until it keeps you ---
print("--- loop ---")
br.loops = 10
br.dread = 0.0
player.global_position = Vector3(0, 0, 18.6)
for i in 8:
await physics_frame
print(" loops=%d dying=%s cause=%s" % [br.loops, br.dying, br.death_cause])
quit()

View File

@ -19,6 +19,28 @@ class_name Backrooms
const TOTEM := "res://assets/store/totem.glb"
## DREAD is the death meter, and it is deliberately the ONLY new number.
##
## A health bar would be a foreign object in this game — nothing else does damage, and
## adding hit points just to have something to subtract from is how you end up with a
## survival-horror UI bolted to a demolition game. So instead: one meter, fed by every
## way this place gets at you, and at 100% you die.
##
## Which death you get depends on WHICH input pushed it over, so the three memes stay
## distinguishable in the moment it matters:
## STARE you looked too long, and the static took you. (Slenderman)
## REACH it got to you. (gory; the xenomorph)
## LOOP you walked the same room too many times. (Blair Witch)
enum Death { NONE, STARE, REACH, LOOP }
const DREAD_FROM_STARE := 0.155 ## per second at full stare pressure
const DREAD_FROM_NEAR := 0.085 ## per second when it's right behind you, unseen
const DREAD_FROM_LOOP := 0.11 ## per wrap
const DREAD_FROM_LUNGS := 0.030 ## per second while you're coughing your guts up
const DREAD_CALM := 0.055 ## per second when nothing is happening to you
## Every loop makes the place better at this. Twelve loops and it barely lets go.
const DREAD_LOOP_GRIP := 0.055
## 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).
@ -26,12 +48,19 @@ const WRAP_COOLDOWN := 1.2
var active := false
var entity: Entity
var dread: Dread
var tape: Dread ## the found-footage layer
var rage: Rage
var dust: Dust
var loops := 0 ## how many times the room has come back around
var caught := 0
var dread := 0.0 ## 0..1 — the death meter
var dying := false
var death_cause: int = Death.NONE
var gore: Gore
var _death_t := 0.0
var _death_from := Transform3D.IDENTITY
var _player: Node3D
var _cam: Camera3D
@ -61,9 +90,13 @@ func setup(player: Node3D, cam: Camera3D, hud: Hud, r: Rage, d: Dust,
dust = d
_host = host
dread = Dread.new()
dread.setup()
host.add_child(dread)
tape = Dread.new()
tape.setup()
host.add_child(tape)
gore = Gore.new()
gore.setup()
host.add_child(gore)
entity = Entity.new()
host.add_child(entity)
@ -79,7 +112,11 @@ func enter(half: float) -> void:
_wrap_cd = 0.0
entity._bounds = half
entity.wake()
dread.set_active(true)
dread = 0.0
dying = false
death_cause = Death.NONE
gore.clear()
tape.set_active(true)
_scatter_totems()
if _hud != null:
_hud.toast("there is no exit. there is no one else here.")
@ -87,7 +124,7 @@ func enter(half: float) -> void:
func leave() -> void:
active = false
entity.sleep()
dread.set_active(false)
tape.set_active(false)
for t in _totems:
if is_instance_valid(t):
t.queue_free()
@ -120,15 +157,23 @@ func _scatter_totems() -> void:
func _process(dt: float) -> void:
if not active or _player == null:
return
if dying:
_tick_death(dt)
return
_wrap_cd = maxf(0.0, _wrap_cd - dt)
_check_wrap()
_tick_dread(dt)
# 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)
tape.drive(dt, entity.static_level, near)
gore.set_blood(clampf((dread - 0.72) * 2.2, 0.0, 0.55))
if dread > 0.72:
gore.arm()
# it whispers when it's close and behind you
_next_whisper -= dt
@ -139,6 +184,93 @@ func _process(dt: float) -> void:
if rage != null:
rage.provoke(0.03, "something is breathing")
## Everything this place does to you lands in one number. Whichever source pushed it
## over the line decides how you die — that's what keeps three deaths readable instead of
## one generic fail.
func _tick_dread(dt: float) -> void:
var src := Death.NONE
var gain := 0.0
if entity.seen_now:
gain += DREAD_FROM_STARE * (0.4 + entity.static_level)
src = Death.STARE
var d := entity.global_position.distance_to(_player.global_position)
if d < 7.0:
var g := DREAD_FROM_NEAR * (1.0 - d / 7.0)
if g > gain:
src = Death.REACH
gain += g
if dust != null and dust.inhaled > 0.6:
gain += DREAD_FROM_LUNGS * dust.inhaled
if gain <= 0.0:
dread = maxf(0.0, dread - DREAD_CALM * dt)
return
# the longer you've been going round, the harder it grips
dread = clampf(dread + gain * dt * (1.0 + loops * DREAD_LOOP_GRIP), 0.0, 1.0)
if dread >= 1.0:
_die(src if src != Death.NONE else Death.STARE)
# ---------------------------------------------------------------- dying
const EPITAPHS := {
Death.STARE: ["YOU LOOKED TOO LONG",
"it was still. it was always still. that was never the safe part."],
Death.REACH: ["IT WAS ALREADY IN THE ROOM",
"you were making so much noise."],
Death.LOOP: ["YOU ARE STILL IN THE ROOM",
"you walked it eleven times. you will walk it again."],
}
func _die(cause: int) -> void:
if dying:
return
dying = true
death_cause = cause
dread = 1.0
_death_t = 0.0
gore.arm()
entity.state = Entity.S.LUNGE
if _cam != null:
_death_from = _cam.transform
# only the physical deaths spray. Being taken by the static shouldn't.
if cause != Death.STARE:
Gore.splatter(_host, _cam.global_position - _cam.global_transform.basis.z * 0.4,
-_cam.global_transform.basis.z)
if _player != null and _player.has_method("set_frozen"):
_player.set_frozen(true)
## Slow on purpose. A jumpscare is a spike; a death should take its time, because the
## seconds spent watching the camera settle onto the carpet are where it lands.
func _tick_death(dt: float) -> void:
_death_t += dt
var t := _death_t
# blood floods the glass over the first second and a half
gore.set_blood(clampf(t / 1.5, 0.0, 1.0))
if _cam != null:
if t < 0.35:
# the hit
_cam.transform = _death_from
_cam.rotation.x = _death_from.basis.get_euler().x + randf_range(-0.30, 0.30)
_cam.rotation.z = randf_range(-0.35, 0.35)
else:
# and then you go down, and the camera lands on its side on the carpet
var k := clampf((t - 0.35) / 2.4, 0.0, 1.0)
k = k * k * (3.0 - 2.0 * k)
_cam.position.y = lerpf(0.0, -1.35, k)
_cam.rotation.z = lerpf(_cam.rotation.z, 1.35, clampf(dt * 2.0, 0.0, 1.0))
_cam.rotation.x = lerpf(_cam.rotation.x, -0.55, clampf(dt * 2.0, 0.0, 1.0))
if t > 2.6:
gore.set_fade(clampf((t - 2.6) / 1.4, 0.0, 1.0))
if t > 4.0 and not _card_shown:
_card_shown = true
var e: Array = EPITAPHS.get(death_cause, EPITAPHS[Death.STARE])
gore.show_card(String(e[0]), String(e[1]))
var _card_shown := false
## 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:
@ -160,11 +292,15 @@ func _check_wrap() -> void:
_player.global_position = p
if _player is CharacterBody3D:
(_player as CharacterBody3D).velocity = Vector3.ZERO
dread.flash()
tape.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
dread = clampf(dread + DREAD_FROM_LOOP, 0.0, 1.0)
if loops >= 11 or dread >= 1.0:
_die(Death.LOOP) # the room keeps you
return
if _hud != null:
_hud.toast(WRAP_LINES[loops % WRAP_LINES.size()])
if rage != null:
@ -172,7 +308,7 @@ func _check_wrap() -> void:
func _on_caught() -> void:
caught += 1
dread.flash()
tape.flash()
if _cam != null:
_cam.rotation.x += randf_range(-0.22, 0.22)
_cam.rotation.z += randf_range(-0.2, 0.2)
@ -182,16 +318,46 @@ func _on_caught() -> void:
dust.inhaled = clampf(dust.inhaled + 0.30, 0.0, 1.0)
if rage != null:
rage.provoke(0.16, "IT TOUCHED YOU")
dread = clampf(dread + 0.34, 0.0, 1.0)
if dread >= 1.0:
_die(Death.REACH)
return
if _hud != null:
_hud.toast("IT TOUCHED YOU")
## R after a death. Rebuild is Main's job; this just clears the horror state.
func revive() -> void:
dying = false
_card_shown = false
death_cause = Death.NONE
dread = 0.0
loops = 0
caught = 0
gore.clear()
if _cam != null:
_cam.position = Vector3.ZERO
_cam.rotation = Vector3.ZERO
if _player != null and _player.has_method("set_frozen"):
_player.set_frozen(false)
entity.wake()
func hud_line() -> String:
if not active:
return ""
if dying:
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]
var label := "steady"
if dread > 0.85:
label = "YOU ARE GOING TO DIE HERE"
elif dread > 0.62:
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]

170
game/scripts/Gore.gd Normal file
View File

@ -0,0 +1,170 @@
extends CanvasLayer
class_name Gore
## Blood on the lens, and the death card.
##
## All procedural — a shader stamping seeded blobs that grow and then bleed downward in
## streaks, plus a burst of red at the camera. No textures, same as everything else here.
##
## The death sequence is deliberately SLOW. A jumpscare is a spike; a death should take
## its time, because the four seconds you spend watching the camera settle onto the
## carpet while blood runs down the glass is where the dread actually lands. Then a card
## that names what got you, so the three deaths stay distinguishable.
##
## Godot 4.7 GDScript 2.0.
const SHADER := """
shader_type canvas_item;
uniform float amount : hint_range(0.0, 1.0) = 0.0;
uniform float t = 0.0;
uniform vec3 tint = vec3(0.42, 0.02, 0.02);
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
void fragment() {
vec2 uv = UV;
float a = 0.0;
// splatter: a fixed set of blobs that swell with `amount`
for (int i = 0; i < 14; i++) {
float fi = float(i);
vec2 c = vec2(hash(vec2(fi, 3.1)), hash(vec2(fi, 7.7)));
float rad = (0.05 + 0.13 * hash(vec2(fi, 11.3))) * amount;
float d = length((uv - c) * vec2(1.0, 0.62));
a += smoothstep(rad, rad * 0.35, d);
// and each blob bleeds downward once it's big enough
float run = clamp((amount - 0.35) * 1.6, 0.0, 1.0) * (0.10 + 0.22 * hash(vec2(fi, 5.5)));
if (uv.y > c.y && uv.y < c.y + run) {
float w = rad * 0.30 * (1.0 - (uv.y - c.y) / max(run, 0.001));
a += smoothstep(w, w * 0.2, abs(uv.x - c.x)) * 0.9;
}
}
// heavier at the edges it's on the housing as well as the glass
vec2 d2 = (uv - vec2(0.5)) * vec2(1.0, 0.62);
a += smoothstep(0.30, 0.95, length(d2) * 2.0) * amount * 1.2;
// clotted texture so it isn't flat
a *= 0.80 + 0.20 * hash(floor(uv * 220.0));
a = clamp(a, 0.0, 1.0) * clamp(amount * 1.35, 0.0, 1.0);
vec3 col = mix(tint, vec3(0.16, 0.0, 0.0), 0.5 + 0.5 * sin(t * 0.7));
COLOR = vec4(col, a);
}
"""
var _blood: ColorRect
var _black: ColorRect
var _card: Control
var _title: Label
var _epitaph: Label
var _hint: Label
var _t := 0.0
var _amount := 0.0
func setup() -> void:
layer = 40 # above the tape and everything else
_blood = ColorRect.new()
_blood.set_anchors_preset(Control.PRESET_FULL_RECT)
_blood.mouse_filter = Control.MOUSE_FILTER_IGNORE
var sh := Shader.new()
sh.code = SHADER
var m := ShaderMaterial.new()
m.shader = sh
_blood.material = m
add_child(_blood)
_black = ColorRect.new()
_black.set_anchors_preset(Control.PRESET_FULL_RECT)
_black.mouse_filter = Control.MOUSE_FILTER_IGNORE
_black.color = Color(0, 0, 0, 0)
add_child(_black)
_card = Control.new()
_card.set_anchors_preset(Control.PRESET_FULL_RECT)
_card.mouse_filter = Control.MOUSE_FILTER_IGNORE
_card.visible = false
add_child(_card)
_title = _label(_card, 46, Color(0.78, 0.06, 0.07), -70)
_epitaph = _label(_card, 20, Color(0.72, 0.70, 0.66), 10)
_hint = _label(_card, 15, Color(0.45, 0.44, 0.42), 74)
set_process(true)
func _label(parent: Control, size: int, col: Color, y: int) -> Label:
var l := Label.new()
l.add_theme_font_size_override("font_size", size)
l.add_theme_color_override("font_color", col)
l.anchor_left = 0.5
l.anchor_right = 0.5
l.anchor_top = 0.5
l.offset_left = -420
l.offset_right = 420
l.offset_top = y
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
parent.add_child(l)
return l
## 0..1. Backrooms drives this up over the death sequence.
func set_blood(v: float) -> void:
_amount = clampf(v, 0.0, 1.0)
func set_fade(v: float) -> void:
_black.color = Color(0, 0, 0, clampf(v, 0.0, 1.0))
func show_card(title: String, epitaph: String) -> void:
_card.visible = true
_title.text = title
_epitaph.text = epitaph
_hint.text = "R — wake up"
func clear() -> void:
_amount = 0.0
_card.visible = false
_black.color = Color(0, 0, 0, 0)
visible = false
func arm() -> void:
visible = true
func _process(dt: float) -> void:
_t += dt
if _blood != null:
var m := _blood.material as ShaderMaterial
m.set_shader_parameter("amount", _amount)
m.set_shader_parameter("t", _t)
## A wet burst at the camera. Called once, on the kill.
static func splatter(host: Node3D, at: Vector3, dir: Vector3) -> void:
var p := GPUParticles3D.new()
p.one_shot = true
p.explosiveness = 1.0
p.amount = 220
p.lifetime = 2.4
p.local_coords = false
var pm := ParticleProcessMaterial.new()
pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
pm.emission_sphere_radius = 0.12
pm.direction = dir
pm.spread = 62.0
pm.initial_velocity_min = 2.5
pm.initial_velocity_max = 9.5
pm.gravity = Vector3(0, -11.0, 0)
pm.scale_min = 0.5
pm.scale_max = 1.9
pm.angular_velocity_min = -600.0
pm.angular_velocity_max = 600.0
p.process_material = pm
var mesh := BoxMesh.new()
mesh.size = Vector3(0.035, 0.035, 0.035)
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.36, 0.015, 0.02)
mat.roughness = 0.35
mesh.material = mat
p.draw_pass_1 = mesh
host.add_child(p)
p.global_position = at
p.emitting = true
p.finished.connect(p.queue_free)

1
game/scripts/Gore.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://bylx3efultyeh

View File

@ -500,6 +500,8 @@ func _unhandled_input(event: InputEvent) -> void:
KEY_B:
_rain(500)
KEY_R:
if _backrooms != null and _backrooms.dying:
_backrooms.revive()
_reset()
KEY_M:
if _modes != null:

View File

@ -36,6 +36,13 @@ var tasks: Tasks = null
var gauntlet: Gauntlet = null
## Set by Main. RMB discharges powder on sites where the smashables are invisible.
var dust: Dust = null
## Set while you're dying, so you can't stroll around during it.
var _frozen := false
func set_frozen(v: bool) -> void:
_frozen = v
if v:
velocity = Vector3.ZERO
var _head: Node3D
var _pitch := 0.0
@ -147,6 +154,9 @@ func _build_crosshair() -> void:
# ---------------------------------------------------------------- input
func _unhandled_input(event: InputEvent) -> void:
# dead men don't switch weapons. R is handled by Main and reaches it regardless.
if _frozen:
return
# a task swallows the keyboard first, so digits and arrows edit the spreadsheet
# instead of switching weapons and walking away from the desk
if tasks != null and tasks.is_working():
@ -250,7 +260,7 @@ func _physics_process(delta: float) -> void:
if Input.is_key_pressed(KEY_SPACE) and is_on_floor():
velocity.y = jump_velocity
var seated := tasks != null and tasks.is_working()
var seated := (tasks != null and tasks.is_working()) or _frozen
var input_dir := Vector2.ZERO
if not seated:
input_dir = Vector2(