extends CanvasLayer class_name RageOverlay ## The blood vessel in your eye, throbbing faster as you lose it. ## ## Three layers, all driven by the same Rage instance: ## 1. a red vignette that closes in as the meter fills, pulsing on the heartbeat ## 2. actual VEINS — a branching tree rooted at the screen edges that grows INWARD as ## the meter fills: thick dark trunks first, fine capillaries only when you're ## really going, and a branch never appears before the trunk carrying it. Generated ## once from a fixed seed so they don't crawl. ## 3. a synthesized lub-dub heartbeat whose rate IS the meter (Rage.bpm) ## ## No textures and no shader files: the vignette is a small ShaderMaterial built in code ## and the veins are draw_polyline, so this drops into a project with zero asset deps. ## ## Godot 4.7 GDScript 2.0. const VIGNETTE_SHADER := """ shader_type canvas_item; uniform float rage : hint_range(0.0, 1.0) = 0.0; uniform float pulse : hint_range(0.0, 1.0) = 0.0; uniform float meltdown : hint_range(0.0, 1.0) = 0.0; void fragment() { // squash y so the vignette matches a wide viewport instead of going circular vec2 d = (UV - vec2(0.5)) * vec2(1.0, 0.62); float r = length(d) * 2.0; // The tunnel closes in as rage climbs and jumps inward on every thump — but the // CENTRE stays readable. You have to be able to see what you're about to destroy, // so this tops out as a heavy frame rather than a red screen. float inner = mix(0.98, 0.44, rage) - pulse * 0.09 * rage; float v = smoothstep(inner, 1.30, r); float a = v * (rage * 0.62 + meltdown * 0.16); vec3 col = mix(vec3(0.52, 0.02, 0.03), vec3(0.75, 0.05, 0.02), meltdown); COLOR = vec4(col, clamp(a, 0.0, 0.95)); } """ var rage: Rage var _vignette: ColorRect var _veins: Control var _heart: AudioStreamPlayer var _beat_wav: AudioStreamWAV var _last_pulse := 0.0 var _paths: Array = [] # {pts: 0..1 screen space, depth, t0, span} func setup(r: Rage) -> void: rage = r layer = 20 # above the HUD _vignette = ColorRect.new() _vignette.set_anchors_preset(Control.PRESET_FULL_RECT) _vignette.mouse_filter = Control.MOUSE_FILTER_IGNORE var sh := Shader.new() sh.code = VIGNETTE_SHADER var m := ShaderMaterial.new() m.shader = sh _vignette.material = m add_child(_vignette) _veins = Control.new() _veins.set_anchors_preset(Control.PRESET_FULL_RECT) _veins.mouse_filter = Control.MOUSE_FILTER_IGNORE _veins.draw.connect(_draw_veins) add_child(_veins) _build_veins() _heart = AudioStreamPlayer.new() _beat_wav = _make_beat() _heart.stream = _beat_wav _heart.volume_db = -6.0 add_child(_heart) # ---------------------------------------------------------------- veins ## Branching polylines rooted at the screen corners and growing inward. Built once with ## a seeded RNG so the pattern is stable — veins that re-randomise every frame read as ## static, not as anatomy. func _build_veins() -> void: var rng := RandomNumberGenerator.new() rng.seed = 0x8100D _paths.clear() var roots := [ [Vector2(0.0, 0.10), Vector2(1.0, 0.6)], [Vector2(0.0, 0.86), Vector2(1.0, -0.5)], [Vector2(1.0, 0.14), Vector2(-1.0, 0.6)], [Vector2(1.0, 0.90), Vector2(-1.0, -0.5)], [Vector2(0.16, 0.0), Vector2(0.4, 1.0)], [Vector2(0.82, 0.0), Vector2(-0.4, 1.0)], [Vector2(0.28, 1.0), Vector2(0.3, -1.0)], [Vector2(0.72, 1.0), Vector2(-0.3, -1.0)], ] for r in roots: _grow(r[0], (r[1] as Vector2).normalized(), 0.30, 5, rng, 0.0, 0.34) ## One segment plus its children. `t0`/`span` are WHERE ALONG THE REVEAL this segment ## lives: a branch cannot appear before the trunk that carries it has finished growing, ## which is the difference between veins creeping in from the edge and a screen of ## disconnected scratches. func _grow(from: Vector2, dir: Vector2, length: float, depth: int, rng: RandomNumberGenerator, t0: float, span: float) -> void: if depth <= 0 or length < 0.02 or t0 >= 1.0: return var pts := PackedVector2Array() pts.append(from) var p := from var d := dir var steps := 4 for i in steps: # wander, but keep being pulled the way the vein set off: a vessel meanders, # it doesn't change its mind d = d.rotated(rng.randf_range(-0.30, 0.30)).lerp(dir, 0.25).normalized() p += d * (length / float(steps)) pts.append(p) _paths.append({"pts": pts, "depth": depth, "t0": t0, "span": span}) if rng.randf() < 0.85: _grow(p, d.rotated(rng.randf_range(0.35, 0.95)), length * 0.62, depth - 1, rng, t0 + span, span * 0.66) if rng.randf() < 0.7: _grow(p, d.rotated(rng.randf_range(-0.95, -0.35)), length * 0.58, depth - 1, rng, t0 + span, span * 0.66) func _draw_veins() -> void: if rage == null: return var r := rage.value if r < 0.12 and not rage.is_meltdown(): return var size := _veins.size var pulse := rage.pulse() var a := clampf((r - 0.10) * 1.25, 0.0, 1.0) if rage.is_meltdown(): a = 1.0 var base_w := (1.1 + 5.0 * r) * (0.80 + 0.55 * pulse) # how far the growth has got. Trunks first; the fine stuff only at real rage. var reveal := clampf(0.18 + r * 1.05, 0.0, 1.0) for e in _paths: var path: Dictionary = e var t0: float = path["t0"] if reveal <= t0: continue # its parent hasn't got here yet var pts: PackedVector2Array = path["pts"] var f := clampf((reveal - t0) / maxf(float(path["span"]), 0.001), 0.0, 1.0) var n := int(ceil(pts.size() * f)) if n < 2: continue var scaled := PackedVector2Array() for i in n: scaled.append(Vector2(pts[i].x * size.x, pts[i].y * size.y)) # thick dark trunks, fine pale capillaries — a flat width for both is what made # them read as biro scribble instead of a vessel var deep := float(path["depth"]) / 5.0 var w := base_w * (0.22 + 0.78 * deep) var col := Color(0.62, 0.04, 0.05, a * (0.22 + 0.40 * pulse) * (0.45 + 0.55 * deep)) _veins.draw_polyline(scaled, col, w, true) # ---------------------------------------------------------------- heartbeat ## A lub-dub built from two damped low sines. Same zero-asset trick as Juice.gd. func _make_beat() -> AudioStreamWAV: var rate := 22050 var dur := 0.42 var n := int(rate * dur) var out := PackedFloat32Array() out.resize(n) for i in n: var t := float(i) / float(rate) var s := 0.0 # lub var e1 := exp(-t * 26.0) s += sin(TAU * 52.0 * t) * e1 * 0.9 s += sin(TAU * 78.0 * t) * e1 * 0.3 # dub, a beat later and softer var t2 := t - 0.16 if t2 > 0.0: var e2 := exp(-t2 * 30.0) s += sin(TAU * 44.0 * t2) * e2 * 0.55 s += sin(TAU * 66.0 * t2) * e2 * 0.2 out[i] = clampf(s, -1.0, 1.0) var wav := AudioStreamWAV.new() wav.format = AudioStreamWAV.FORMAT_16_BITS wav.mix_rate = rate wav.stereo = false var bytes := PackedByteArray() bytes.resize(out.size() * 2) for i in out.size(): bytes.encode_s16(i * 2, int(out[i] * 32767.0)) wav.data = bytes return wav # ---------------------------------------------------------------- per-frame func _process(_dt: float) -> void: if rage == null: return var m := _vignette.material as ShaderMaterial var p := rage.pulse() m.set_shader_parameter("rage", rage.value) m.set_shader_parameter("pulse", p) m.set_shader_parameter("meltdown", 1.0 if rage.is_meltdown() else 0.0) _veins.queue_redraw() # fire the thump on the rising edge of each beat if p > 0.55 and _last_pulse <= 0.55 and (rage.value > 0.18 or rage.is_meltdown()): _heart.volume_db = lerpf(-20.0, -3.0, clampf(rage.value, 0.0, 1.0)) _heart.pitch_scale = lerpf(0.92, 1.16, rage.value) _heart.play() _last_pulse = p