- assets/audio/music/zombie_industrial_loop.ogg: 57.60 s Ogg Vorbis loop generated locally with YuE2-3B on the Tesla V100 (score written for this track; vocals removed with HTDemucs). The loop is spliced at the track's structural repeat with a 0.6 s crossfade: measured seam dip <= 1.6 dB. - scripts/main.gd: "Music" AudioStreamPlayer at -11 dB, started at boot on desktop and on the start click on Web (browser autoplay policy). It forces stream.loop at runtime because the Web staging step re-imports the OGG with loop=false. - tools/test_music.gd: soundtrack test (loop, length, volume, playback, still playing after player death). - docs/MUSIC.md: source, loop construction, verification, licence. - docs/WEB-DEPLOY.md: release ab6054111f5919a8 notes. Initial commit of the project.
91 lines
3.3 KiB
GDScript
91 lines
3.3 KiB
GDScript
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()
|