- 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.
94 lines
3.1 KiB
GDScript
94 lines
3.1 KiB
GDScript
extends CharacterBody3D
|
|
signal killed(headshot: bool)
|
|
var player: CharacterBody3D
|
|
var hp := 100.0
|
|
var speed := 2.0
|
|
var cooldown := 0.0
|
|
var dead := false
|
|
var entering := true
|
|
var anim: AnimationPlayer
|
|
var voice_timer := 4.0
|
|
var entry_tween: Tween
|
|
const Audio = preload("res://scripts/hl_audio.gd")
|
|
|
|
func _ready() -> void:
|
|
add_to_group("live_zombies")
|
|
collision_layer = 4
|
|
collision_mask = 1 | 4
|
|
var shape := CollisionShape3D.new()
|
|
var cap := CapsuleShape3D.new()
|
|
cap.radius = 0.35
|
|
cap.height = 1.8
|
|
shape.shape = cap
|
|
shape.position.y = 0.9
|
|
add_child(shape)
|
|
var model: Node3D = load("res://assets/zombies/zombie_textured.scn").instantiate()
|
|
add_child(model)
|
|
anim = model.get_node("AnimationPlayer")
|
|
for mesh in model.find_children("*", "MeshInstance3D", true, false):
|
|
mesh.layers = 2
|
|
for area in model.find_children("*", "Area3D", true, false):
|
|
area.collision_layer = 0
|
|
speed = randf_range(1.6, 2.8)
|
|
anim.play("walk")
|
|
Audio.play(self, "zombie/zo_alert%d0.wav" % randi_range(1, 3), true, -15)
|
|
|
|
func _physics_process(dt: float) -> void:
|
|
if dead or entering or not is_instance_valid(player) or player.hp <= 0:
|
|
return
|
|
cooldown -= dt
|
|
voice_timer -= dt
|
|
if voice_timer <= 0:
|
|
voice_timer = randf_range(5, 10)
|
|
Audio.play(self, "zombie/zo_idle%d.wav" % randi_range(1, 4), true, -19)
|
|
var dir := player.global_position - global_position
|
|
dir.y = 0
|
|
var distance := dir.length()
|
|
if distance > 0.01:
|
|
rotation.y = atan2(-dir.x, -dir.z)
|
|
var walking := distance > 1.7
|
|
var motion := dir.normalized() * speed if walking else Vector3.ZERO
|
|
velocity.x = motion.x
|
|
velocity.z = motion.z
|
|
velocity.y = -1.0 if is_on_floor() else velocity.y - 20.0 * dt
|
|
move_and_slide()
|
|
if walking and cooldown <= 0 and anim.current_animation != "walk":
|
|
anim.play("walk", 0.15)
|
|
if not walking and cooldown <= 0:
|
|
cooldown = 1.5
|
|
anim.play("attack2", 0.1)
|
|
Audio.play(self, "zombie/zo_attack%d.wav" % randi_range(1, 2), true, -15)
|
|
get_tree().create_timer(0.5).timeout.connect(_attack)
|
|
|
|
func _attack() -> void:
|
|
if not dead and is_instance_valid(player) and player.hp > 0 and global_position.distance_to(player.global_position) < 2.3:
|
|
player.take_damage(12)
|
|
Audio.play(self, "zombie/claw_strike%d.wav" % randi_range(1, 3), true, -12)
|
|
|
|
func take_damage(amount: float, hit_y: float) -> void:
|
|
if dead:
|
|
return
|
|
var headshot := hit_y - global_position.y > 1.45
|
|
hp -= amount * (2.5 if headshot else 1.0)
|
|
if hp <= 0:
|
|
dead = true
|
|
remove_from_group("live_zombies")
|
|
velocity = Vector3.ZERO
|
|
if entry_tween and entry_tween.is_running():
|
|
entry_tween.kill()
|
|
collision_layer = 0
|
|
collision_mask = 0
|
|
for speaker in get_children():
|
|
if speaker is AudioStreamPlayer3D:
|
|
speaker.stop()
|
|
speaker.queue_free()
|
|
# The original zombie has no dedicated death WAV: use its pain grunt only at death.
|
|
Audio.play(self, "zombie/zo_pain2.wav", true, -11)
|
|
anim.play("dieheadshot" if headshot else "diesimple", 0.1)
|
|
var settle := create_tween()
|
|
settle.tween_property(self, "position:y", 0.0, 0.45)
|
|
add_to_group("zombie_corpses")
|
|
killed.emit(headshot)
|
|
else:
|
|
Audio.play(self, "zombie/zo_pain1.wav", true, -15)
|