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 — branching polylines creeping in from the corners, thickening ## with each thump. 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 = [] # each entry: PackedVector2Array in 0..1 screen space 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) func _grow(from: Vector2, dir: Vector2, length: float, depth: int, rng: RandomNumberGenerator) -> void: if depth <= 0 or length < 0.02: return var pts := PackedVector2Array() pts.append(from) var p := from var d := dir var steps := 4 for i in steps: d = d.rotated(rng.randf_range(-0.42, 0.42)).normalized() p += d * (length / float(steps)) pts.append(p) _paths.append(pts) # branch if rng.randf() < 0.85: _grow(p, d.rotated(rng.randf_range(0.35, 0.95)), length * 0.62, depth - 1, rng) if rng.randf() < 0.7: _grow(p, d.rotated(rng.randf_range(-0.95, -0.35)), length * 0.58, depth - 1, rng) 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 col := Color(0.62, 0.04, 0.05, a * (0.30 + 0.45 * pulse)) var w := (1.4 + 4.2 * r) * (0.80 + 0.55 * pulse) # only the fraction of each vein proportional to rage is drawn, so they visibly # creep further into the frame the angrier you get var reveal := clampf(0.25 + r * 0.85, 0.0, 1.0) for path in _paths: var pts: PackedVector2Array = path var n := int(ceil(pts.size() * reveal)) if n < 2: continue var scaled := PackedVector2Array() for i in n: scaled.append(Vector2(pts[i].x * size.x, pts[i].y * size.y)) _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