Files
zombie-fps/scripts/zombie.gd
T

136 lines
4.6 KiB
GDScript

extends CharacterBody3D
signal killed(headshot: bool)
var player: CharacterBody3D
var navigation: Node
var _path := PackedVector3Array()
var _path_index := 0
var _repath := 0.0
var _direct := false
var _nav_started := false
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)
_repath = randf_range(0, 0.5)
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 or not _attack_clear()
var motion := _pursuit_direction(dt) * speed if walking else Vector3.ZERO
if motion.length_squared() > 0.01:
rotation.y = atan2(-motion.x, -motion.z)
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 _pursuit_direction(dt: float) -> Vector3:
var target := player.global_position
_repath -= dt
if navigation != null and (_repath <= 0 or not _nav_started):
_nav_started = true
_repath = 0.55 + float(get_instance_id() % 7) * 0.02
_direct = navigation.direct_clear(global_position, target)
if not _direct:
_path = navigation.route(global_position, target)
_path_index = 0
if _path.size() > 1 and navigation.direct_clear(global_position, _path[1]):
_path_index = 1
if navigation == null or _direct:
var dir := target - global_position
dir.y = 0
return dir.normalized()
while _path_index < _path.size():
var dir := _path[_path_index] - global_position
dir.y = 0
if dir.length() > 0.22:
return dir.normalized()
_path_index += 1
return Vector3.ZERO
func _attack_clear() -> bool:
# Check below desk-top height: a chest-high ray can pass over a desk
# even though the bodies cannot approach each other through it.
var start := global_position + Vector3.UP * 0.6
var end := player.global_position + Vector3.UP * 0.6
var query := PhysicsRayQueryParameters3D.create(start, end, 1)
query.exclude = [get_rid(), player.get_rid()]
return get_world_3d().direct_space_state.intersect_ray(query).is_empty()
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 and _attack_clear():
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)