diff --git a/LANES/LANE7-cubicle-hell.md b/LANES/LANE7-cubicle-hell.md index 8bb5acd..bdacccd 100644 --- a/LANES/LANE7-cubicle-hell.md +++ b/LANES/LANE7-cubicle-hell.md @@ -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. diff --git a/game/README.md b/game/README.md index bc60477..4309efd 100644 --- a/game/README.md +++ b/game/README.md @@ -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 diff --git a/game/dev/DemoDriver.gd b/game/dev/DemoDriver.gd index df8f674..43beb15 100644 --- a/game/dev/DemoDriver.gd +++ b/game/dev/DemoDriver.gd @@ -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: diff --git a/game/dev/probe_death.gd b/game/dev/probe_death.gd new file mode 100644 index 0000000..d3e21c4 --- /dev/null +++ b/game/dev/probe_death.gd @@ -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() diff --git a/game/scripts/Backrooms.gd b/game/scripts/Backrooms.gd index cc04515..b08d188 100644 --- a/game/scripts/Backrooms.gd +++ b/game/scripts/Backrooms.gd @@ -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] diff --git a/game/scripts/Gore.gd b/game/scripts/Gore.gd new file mode 100644 index 0000000..c6c50a7 --- /dev/null +++ b/game/scripts/Gore.gd @@ -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) diff --git a/game/scripts/Gore.gd.uid b/game/scripts/Gore.gd.uid new file mode 100644 index 0000000..682a1f4 --- /dev/null +++ b/game/scripts/Gore.gd.uid @@ -0,0 +1 @@ +uid://bylx3efultyeh diff --git a/game/scripts/Main.gd b/game/scripts/Main.gd index 29b8205..03d8ab6 100644 --- a/game/scripts/Main.gd +++ b/game/scripts/Main.gd @@ -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: diff --git a/game/scripts/Player.gd b/game/scripts/Player.gd index fb497b9..666dc19 100644 --- a/game/scripts/Player.gd +++ b/game/scripts/Player.gd @@ -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(