destroyulator/game/scripts/Gore.gd
Monster Robot Party bcd67906a4 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>
2026-07-31 19:44:26 +10:00

171 lines
5.0 KiB
GDScript

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)