extends CanvasLayer ## Local-only trauma pivot: never modifies the player's aim angles. const SHAKE_SECONDS := 0.42 const BLOOD_SECONDS := 1.15 const SPLAT_COUNT := 7 var pivot: Node3D var shake_left := 0.0 var blood_left := 0.0 var intensity := 0.0 var phase := 0.0 var active := true var overlay: Control var vignette: ColorRect var shader_material: ShaderMaterial var splats: Array[TextureRect] = [] func _ready() -> void: layer = 2 overlay = Control.new() overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(overlay) overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) vignette = ColorRect.new() vignette.mouse_filter = Control.MOUSE_FILTER_IGNORE shader_material = ShaderMaterial.new() shader_material.shader = load("res://assets/effects/damage_vignette.gdshader") vignette.material = shader_material overlay.add_child(vignette) vignette.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) for i in SPLAT_COUNT: var splat := TextureRect.new() splat.texture = load("res://assets/effects/blood.png") splat.expand_mode = TextureRect.EXPAND_IGNORE_SIZE splat.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED splat.mouse_filter = Control.MOUSE_FILTER_IGNORE splat.modulate.a = 0 overlay.add_child(splat) splats.append(splat) func hit(amount: float) -> void: if not active or amount <= 0: return # Refresh instead of stacking tweens/particles: bounded cost and shake. intensity = minf(1.0, maxf(intensity, 0.55 + amount / 100.0) + 0.12) shake_left = SHAKE_SECONDS blood_left = BLOOD_SECONDS phase = randf_range(0, TAU) var viewport_size := get_viewport().get_visible_rect().size for i in splats.size(): var splat := splats[i] var size := minf(viewport_size.x, viewport_size.y) * randf_range(0.22, 0.38) splat.size = Vector2.ONE * size splat.pivot_offset = splat.size * 0.5 # Keep the central reticle and HUD readable; splats lie near the edges. var center := Vector2.ZERO match i % 4: 0: center = viewport_size * Vector2(randf_range(0.06, 0.17), randf_range(0.25, 0.90)) 1: center = viewport_size * Vector2(randf_range(0.83, 0.95), randf_range(0.25, 0.90)) 2: center = viewport_size * Vector2(randf_range(0.25, 0.75), randf_range(0.82, 0.98)) 3: center = viewport_size * Vector2(randf_range(0.3, 0.8), randf_range(0.04, 0.13)) splat.position = center - splat.size * 0.5 splat.rotation = randf_range(-PI, PI) splat.modulate = Color(0.85, 0.1, 0.12, 0.85 * intensity) shader_material.set_shader_parameter("strength", intensity) func _process(delta: float) -> void: if not active: return phase += delta * 42.0 shake_left = maxf(0, shake_left - delta) blood_left = maxf(0, blood_left - delta) var shake := pow(shake_left / SHAKE_SECONDS, 2.0) * intensity if is_instance_valid(pivot): pivot.position = Vector3(sin(phase) * 0.025, cos(phase * 1.3) * 0.018, sin(phase * 0.8) * 0.012) * shake pivot.rotation = Vector3(sin(phase * 0.9) * 0.024, cos(phase) * 0.013, sin(phase * 1.1) * 0.035) * shake var alpha := blood_left / BLOOD_SECONDS for splat in splats: splat.modulate.a = alpha * 0.85 * intensity shader_material.set_shader_parameter("strength", minf(1, alpha * 1.4) * intensity) if blood_left <= 0: intensity = 0 func stop() -> void: active = false shake_left = 0 blood_left = 0 intensity = 0 if is_instance_valid(pivot): pivot.transform = Transform3D.IDENTITY overlay.hide()