Add industrial metal soundtrack with seamless loop
- 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.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
extends RefCounted
|
||||
const MAX_STAINS := 160
|
||||
|
||||
static func impact(world: Node3D, point: Vector3, direction: Vector3) -> void:
|
||||
# CPU particles work in both Forward+ and WebGL Compatibility.
|
||||
var particles := CPUParticles3D.new()
|
||||
particles.name = "BloodSpray"
|
||||
particles.amount = 28
|
||||
particles.lifetime = 0.65
|
||||
particles.one_shot = true
|
||||
particles.explosiveness = 1.0
|
||||
particles.direction = direction.normalized()
|
||||
particles.spread = 65
|
||||
particles.initial_velocity_min = 1.3
|
||||
particles.initial_velocity_max = 3.5
|
||||
particles.gravity = Vector3(0, -9.8, 0)
|
||||
particles.scale_amount_min = 0.5
|
||||
particles.scale_amount_max = 1.5
|
||||
var mesh := SphereMesh.new()
|
||||
mesh.radius = 0.018
|
||||
mesh.height = 0.036
|
||||
var material := StandardMaterial3D.new()
|
||||
material.albedo_color = Color(0.45, 0.015, 0.025)
|
||||
material.roughness = 0.35
|
||||
mesh.material = material
|
||||
particles.mesh = mesh
|
||||
world.add_child(particles)
|
||||
particles.global_position = point
|
||||
particles.emitting = true
|
||||
world.get_tree().create_timer(1.3).timeout.connect(particles.queue_free)
|
||||
var query := PhysicsRayQueryParameters3D.create(point + Vector3.UP * 0.05, point + Vector3.DOWN * 8, 1)
|
||||
var hit := world.get_world_3d().direct_space_state.intersect_ray(query)
|
||||
if hit.is_empty() or hit.normal.dot(Vector3.UP) < 0.7:
|
||||
return
|
||||
var width := randf_range(0.55, 1.15)
|
||||
var stain: Node3D
|
||||
if RenderingServer.get_current_rendering_method() == "gl_compatibility":
|
||||
# WebGL has no projected decals: textured horizontal patch on floor.
|
||||
var patch := MeshInstance3D.new()
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(width, width)
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_texture = load("res://assets/effects/blood.png")
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||
mat.roughness = 0.9
|
||||
plane.material = mat
|
||||
patch.mesh = plane
|
||||
patch.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
stain = patch
|
||||
else:
|
||||
var decal := Decal.new()
|
||||
decal.texture_albedo = load("res://assets/effects/blood.png")
|
||||
decal.size = Vector3(width, 0.25, width)
|
||||
decal.cull_mask = 1
|
||||
decal.upper_fade = 0
|
||||
decal.lower_fade = 0
|
||||
stain = decal
|
||||
stain.name = "BloodStain"
|
||||
world.add_child(stain)
|
||||
stain.global_position = hit.position + Vector3.UP * 0.012
|
||||
stain.rotation.y = randf_range(0, TAU)
|
||||
stain.add_to_group("blood_stains")
|
||||
var stains := world.get_tree().get_nodes_in_group("blood_stains")
|
||||
if stains.size() > MAX_STAINS:
|
||||
stains[0].queue_free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dwi0onjbbv86u
|
||||
@@ -0,0 +1,90 @@
|
||||
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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://75qn70d6fklx
|
||||
@@ -0,0 +1,113 @@
|
||||
extends RigidBody3D
|
||||
signal detonated
|
||||
const FUSE_SECONDS := 4.0
|
||||
const BLAST_RADIUS := 5.0
|
||||
const MAX_DAMAGE := 160.0
|
||||
const Audio = preload("res://scripts/hl_audio.gd")
|
||||
const Blood = preload("res://scripts/blood_effects.gd")
|
||||
var age := 0.0
|
||||
var exploded := false
|
||||
var thrower: CharacterBody3D
|
||||
|
||||
func _ready() -> void:
|
||||
name = "Grenade"
|
||||
add_to_group("grenades")
|
||||
collision_layer = 16
|
||||
collision_mask = 1 | 4
|
||||
mass = 0.4
|
||||
continuous_cd = true
|
||||
linear_damp = 0.15
|
||||
angular_damp = 0.6
|
||||
var physics := PhysicsMaterial.new()
|
||||
physics.bounce = 0.45
|
||||
physics.friction = 0.65
|
||||
physics_material_override = physics
|
||||
var collider := CollisionShape3D.new()
|
||||
var sphere := SphereShape3D.new()
|
||||
sphere.radius = 0.12
|
||||
collider.shape = sphere
|
||||
add_child(collider)
|
||||
var model: Node3D = load("res://assets/weapons/grenade.scn").instantiate()
|
||||
add_child(model)
|
||||
for area in model.find_children("*", "Area3D", true, false):
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 0
|
||||
for mesh in model.find_children("*", "MeshInstance3D", true, false):
|
||||
mesh.layers = 2
|
||||
if is_instance_valid(thrower):
|
||||
add_collision_exception_with(thrower)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
age += delta
|
||||
if age >= FUSE_SECONDS and not exploded:
|
||||
exploded = true
|
||||
call_deferred("_explode")
|
||||
|
||||
func _explode() -> void:
|
||||
freeze = true
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
hide()
|
||||
var world := get_parent() as Node3D
|
||||
var point := global_position
|
||||
var targets := get_tree().get_nodes_in_group("live_zombies")
|
||||
if is_instance_valid(thrower) and not thrower.death_started:
|
||||
targets.append(thrower)
|
||||
for target in targets:
|
||||
if not is_instance_valid(target):
|
||||
continue
|
||||
var center: Vector3 = target.global_position + Vector3.UP * 0.9
|
||||
var distance := point.distance_to(center)
|
||||
if distance > BLAST_RADIUS:
|
||||
continue
|
||||
var query := PhysicsRayQueryParameters3D.create(point, center, 1)
|
||||
if not get_world_3d().direct_space_state.intersect_ray(query).is_empty():
|
||||
continue
|
||||
var damage := MAX_DAMAGE * (1.0 - distance / BLAST_RADIUS)
|
||||
if target == thrower:
|
||||
target.take_damage(damage)
|
||||
else:
|
||||
Blood.impact(world, center, (center - point).normalized())
|
||||
target.take_damage(damage, center.y)
|
||||
_effect(world, point)
|
||||
detonated.emit()
|
||||
queue_free()
|
||||
|
||||
func _effect(world: Node3D, point: Vector3) -> void:
|
||||
var effect := Node3D.new()
|
||||
effect.name = "GrenadeExplosion"
|
||||
world.add_child(effect)
|
||||
effect.global_position = point
|
||||
Audio.play(effect, "weapons/explode%d.wav" % randi_range(3, 5), true, -7)
|
||||
var light := OmniLight3D.new()
|
||||
light.light_color = Color(1, 0.45, 0.08)
|
||||
light.light_energy = 8
|
||||
light.omni_range = 10
|
||||
effect.add_child(light)
|
||||
var tween := effect.create_tween()
|
||||
tween.tween_property(light, "light_energy", 0.0, 0.3)
|
||||
var particles := CPUParticles3D.new()
|
||||
particles.amount = 60
|
||||
particles.lifetime = 0.85
|
||||
particles.one_shot = true
|
||||
particles.explosiveness = 1
|
||||
particles.direction = Vector3.UP
|
||||
particles.spread = 180
|
||||
particles.initial_velocity_min = 2
|
||||
particles.initial_velocity_max = 6
|
||||
particles.gravity = Vector3(0, -3, 0)
|
||||
var mesh := SphereMesh.new()
|
||||
mesh.radius = 0.06
|
||||
mesh.height = 0.12
|
||||
var material := StandardMaterial3D.new()
|
||||
material.albedo_color = Color(1, 0.3, 0.05)
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mesh.material = material
|
||||
particles.mesh = mesh
|
||||
var colors := Gradient.new()
|
||||
colors.set_color(0, Color(1, 0.65, 0.1, 1))
|
||||
colors.set_color(1, Color(0.15, 0.12, 0.1, 0))
|
||||
particles.color_ramp = colors
|
||||
effect.add_child(particles)
|
||||
particles.emitting = true
|
||||
world.get_tree().create_timer(4).timeout.connect(effect.queue_free)
|
||||
@@ -0,0 +1 @@
|
||||
uid://6681xr24ymu0
|
||||
@@ -0,0 +1,22 @@
|
||||
extends RefCounted
|
||||
## Original WAVs; overlapping shots allowed, positional creature/environment audio.
|
||||
static func play(parent: Node, path: String, positional: bool = false, volume: float = -10.0) -> void:
|
||||
var stream := load("res://assets/audio/" + path) as AudioStream
|
||||
if stream == null:
|
||||
return
|
||||
if positional:
|
||||
var speaker := AudioStreamPlayer3D.new()
|
||||
speaker.stream = stream
|
||||
speaker.volume_db = volume
|
||||
speaker.max_distance = 30
|
||||
speaker.unit_size = 4
|
||||
parent.add_child(speaker)
|
||||
speaker.finished.connect(speaker.queue_free)
|
||||
speaker.play()
|
||||
else:
|
||||
var speaker := AudioStreamPlayer.new()
|
||||
speaker.stream = stream
|
||||
speaker.volume_db = volume
|
||||
parent.add_child(speaker)
|
||||
speaker.finished.connect(speaker.queue_free)
|
||||
speaker.play()
|
||||
@@ -0,0 +1 @@
|
||||
uid://clxd1ronsgnm6
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
extends Node3D
|
||||
const PlayerScript = preload("res://scripts/player.gd")
|
||||
const ZombieScript = preload("res://scripts/zombie.gd")
|
||||
const RoomScript = preload("res://scripts/room.gd")
|
||||
const MUSIC_PATH := "res://assets/audio/music/zombie_industrial_loop.ogg"
|
||||
const MUSIC_VOLUME_DB := -11.0
|
||||
var player: CharacterBody3D
|
||||
var room: Node3D
|
||||
var music: AudioStreamPlayer
|
||||
var music_started := false
|
||||
var hud: Label
|
||||
var score := 0
|
||||
var wave := 0
|
||||
var alive := 0
|
||||
var spawning := false
|
||||
var wave_pending := false
|
||||
var smoke := false
|
||||
var awaiting_web_start := false
|
||||
var start_label: Label
|
||||
|
||||
func _ready() -> void:
|
||||
smoke = "--smoke" in OS.get_cmdline_user_args()
|
||||
room = Node3D.new()
|
||||
room.set_script(RoomScript)
|
||||
add_child(room)
|
||||
player = CharacterBody3D.new()
|
||||
player.set_script(PlayerScript)
|
||||
player.position = Vector3(0, 0.1, 0)
|
||||
add_child(player)
|
||||
var envnode := WorldEnvironment.new()
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = Color(0.08, 0.12, 0.19)
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color(0.65, 0.73, 0.85)
|
||||
env.ambient_light_energy = 0.65
|
||||
envnode.environment = env
|
||||
add_child(envnode)
|
||||
var light := OmniLight3D.new()
|
||||
light.position = Vector3(0, 4.5, 0)
|
||||
light.omni_range = 24
|
||||
light.light_energy = 3
|
||||
light.shadow_enabled = true
|
||||
add_child(light)
|
||||
var canvas := CanvasLayer.new()
|
||||
add_child(canvas)
|
||||
hud = Label.new()
|
||||
hud.position = Vector2(24, 20)
|
||||
hud.add_theme_font_size_override("font_size", 24)
|
||||
canvas.add_child(hud)
|
||||
var cross := Label.new()
|
||||
cross.text = "+"
|
||||
cross.add_theme_font_size_override("font_size", 30)
|
||||
canvas.add_child(cross)
|
||||
cross.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||
_setup_music()
|
||||
if OS.has_feature("web"):
|
||||
awaiting_web_start = true
|
||||
start_label = Label.new()
|
||||
start_label.text = "CLICCA PER INIZIARE\nWASD: movimento · clic sinistro: spara · destro: granata · R: ricarica"
|
||||
start_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
start_label.add_theme_font_size_override("font_size", 24)
|
||||
start_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
canvas.add_child(start_label)
|
||||
start_label.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||
else:
|
||||
_start_music()
|
||||
_next_wave()
|
||||
if smoke:
|
||||
get_tree().create_timer(12).timeout.connect(func():
|
||||
print("SMOKE: wave=", wave, " alive=", alive, " player_hp=", player.hp)
|
||||
get_tree().quit())
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if awaiting_web_start and event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
awaiting_web_start = false
|
||||
start_label.queue_free()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
# Il browser concede l'audio solo dopo un gesto dell'utente: qui il click.
|
||||
_start_music()
|
||||
_next_wave()
|
||||
|
||||
## Colonna sonora: industrial metal strumentale su loop senza giunzione udibile.
|
||||
func _setup_music() -> void:
|
||||
music = AudioStreamPlayer.new()
|
||||
music.name = "Music"
|
||||
music.stream = load(MUSIC_PATH)
|
||||
music.volume_db = MUSIC_VOLUME_DB
|
||||
if music.stream is AudioStreamOggVorbis:
|
||||
music.stream.loop = true
|
||||
add_child(music)
|
||||
music.add_to_group("music")
|
||||
# Rete di sicurezza: se il flag di loop mancasse, il brano riparte a fine coda.
|
||||
music.finished.connect(_restart_music)
|
||||
|
||||
func _start_music() -> void:
|
||||
if music_started or music == null:
|
||||
return
|
||||
music_started = true
|
||||
music.play()
|
||||
|
||||
func _restart_music() -> void:
|
||||
if music_started and is_instance_valid(music):
|
||||
music.play()
|
||||
|
||||
func _process(_dt: float) -> void:
|
||||
if hud:
|
||||
hud.text = "VITA %d MUNIZIONI %d/30 ONDATA %d PUNTI %d\nWASD · mouse · sinistro: spara · destro: granata · R: ricarica · spazio: salto · Esc: mouse" % [player.hp, player.ammo, wave, score]
|
||||
hud.text += "\nGRANATA: " + ("PRONTA" if player.grenade_cooldown <= 0 else "%d s" % ceili(player.grenade_cooldown))
|
||||
if player.reloading:
|
||||
hud.text += "\nRICARICA..."
|
||||
if player.hp <= 0:
|
||||
hud.text += "\nSEI MORTO — premi R per ricominciare"
|
||||
|
||||
func zombies_for_wave(number: int) -> int:
|
||||
return 4 + number * 2
|
||||
|
||||
func _next_wave() -> void:
|
||||
wave_pending = false
|
||||
if player.hp <= 0 or spawning:
|
||||
return
|
||||
wave += 1
|
||||
spawning = true
|
||||
for i in zombies_for_wave(wave):
|
||||
if player.hp <= 0:
|
||||
break
|
||||
_spawn(i % 4)
|
||||
await get_tree().create_timer(0.8).timeout
|
||||
spawning = false
|
||||
_check_wave()
|
||||
|
||||
func _spawn(index: int) -> void:
|
||||
var centers := [Vector3(0, 0, -9), Vector3(0, 0, 9), Vector3(-9, 0, 0), Vector3(9, 0, 0)]
|
||||
var inward: Vector3 = -centers[index].normalized()
|
||||
var zombie := CharacterBody3D.new()
|
||||
zombie.set_script(ZombieScript)
|
||||
zombie.player = player
|
||||
zombie.position = centers[index] - inward * 1.2
|
||||
add_child(zombie)
|
||||
alive += 1
|
||||
zombie.killed.connect(func(headshot: bool):
|
||||
score += 150 if headshot else 100
|
||||
alive -= 1
|
||||
_check_wave())
|
||||
var tween := zombie.create_tween()
|
||||
zombie.entry_tween = tween
|
||||
tween.tween_property(zombie, "position", centers[index] + Vector3(0, 1.55, 0), 1.2)
|
||||
tween.tween_callback(func():
|
||||
for glass in room.glass_panes.duplicate():
|
||||
if is_instance_valid(glass) and Vector2(glass.position.x - centers[index].x, glass.position.z - centers[index].z).length() < 2:
|
||||
room.shatter_glass(glass))
|
||||
tween.tween_property(zombie, "position", centers[index] + inward * 1.4 + Vector3(0, 0.1, 0), 0.8)
|
||||
tween.tween_callback(func(): zombie.entering = false)
|
||||
|
||||
func _check_wave() -> void:
|
||||
if alive == 0 and not spawning and not wave_pending and player.hp > 0:
|
||||
wave_pending = true
|
||||
get_tree().create_timer(3).timeout.connect(_next_wave)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bc8p55iv7cox6
|
||||
@@ -0,0 +1,198 @@
|
||||
extends CharacterBody3D
|
||||
var camera: Camera3D
|
||||
var damage_pivot: Node3D
|
||||
var damage_feedback: CanvasLayer
|
||||
const DamageFeedback = preload("res://scripts/damage_feedback.gd")
|
||||
var hp := 100.0
|
||||
var ammo := 30
|
||||
var cooldown := 0.0
|
||||
var grenade_cooldown := 0.0
|
||||
const GRENADE_COOLDOWN := 15.0
|
||||
const Grenade = preload("res://scripts/grenade.gd")
|
||||
var reloading := false
|
||||
var gun: Node3D
|
||||
var flash: OmniLight3D
|
||||
var weapon_anim: AnimationPlayer
|
||||
var step_timer := 0.0
|
||||
var death_started := false
|
||||
var death_overlay: ColorRect
|
||||
const Blood = preload("res://scripts/blood_effects.gd")
|
||||
const Audio = preload("res://scripts/hl_audio.gd")
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 2
|
||||
collision_mask = 1 | 4
|
||||
var cs := CollisionShape3D.new()
|
||||
var cap := CapsuleShape3D.new()
|
||||
cap.height = 1.8
|
||||
cap.radius = 0.35
|
||||
cs.shape = cap
|
||||
cs.position.y = 0.9
|
||||
add_child(cs)
|
||||
camera = Camera3D.new()
|
||||
camera.position.y = 1.65
|
||||
camera.current = true
|
||||
camera.fov = 80
|
||||
damage_pivot = Node3D.new()
|
||||
damage_pivot.name = "DamagePivot"
|
||||
add_child(damage_pivot)
|
||||
damage_pivot.add_child(camera)
|
||||
damage_feedback = CanvasLayer.new()
|
||||
damage_feedback.set_script(DamageFeedback)
|
||||
damage_feedback.pivot = damage_pivot
|
||||
add_child(damage_feedback)
|
||||
gun = Node3D.new()
|
||||
camera.add_child(gun)
|
||||
var viewmodel: Node3D = load("res://assets/weapons/v_9mmar.scn").instantiate()
|
||||
gun.add_child(viewmodel)
|
||||
for area in viewmodel.find_children("*", "Area3D", true, false):
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 0
|
||||
for mesh in viewmodel.find_children("*", "MeshInstance3D", true, false):
|
||||
mesh.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
mesh.layers = 2
|
||||
weapon_anim = viewmodel.get_node("AnimationPlayer")
|
||||
weapon_anim.animation_finished.connect(func(_name: StringName):
|
||||
if not reloading and not death_started:
|
||||
weapon_anim.play("idle1"))
|
||||
weapon_anim.play("deploy")
|
||||
flash = OmniLight3D.new()
|
||||
flash.position = Vector3(0.28, -0.2, -1.1)
|
||||
flash.light_color = Color(1, 0.7, 0.25)
|
||||
flash.omni_range = 7
|
||||
flash.light_energy = 0
|
||||
camera.add_child(flash)
|
||||
if DisplayServer.get_name() != "headless" and not OS.has_feature("web"):
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if OS.has_feature("web") and event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT and not death_started:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED and hp > 0:
|
||||
rotate_y(-event.relative.x * 0.0025)
|
||||
camera.rotation.x = clampf(camera.rotation.x - event.relative.y * 0.0025, -1.4, 1.4)
|
||||
if event.is_action_pressed("pause") and not death_started:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED else Input.MOUSE_MODE_CAPTURED
|
||||
if event.is_action_pressed("grenade") and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED and not death_started:
|
||||
throw_grenade()
|
||||
if event.is_action_pressed("reload"):
|
||||
if hp <= 0:
|
||||
get_tree().reload_current_scene()
|
||||
elif not reloading and ammo < 30:
|
||||
reloading = true
|
||||
weapon_anim.play("reload")
|
||||
Audio.play(self, "items/cliprelease1.wav")
|
||||
get_tree().create_timer(0.8).timeout.connect(func():
|
||||
if not death_started:
|
||||
Audio.play(self, "items/clipinsert1.wav"))
|
||||
get_tree().create_timer(1.57).timeout.connect(func():
|
||||
if not death_started:
|
||||
ammo = 30
|
||||
reloading = false
|
||||
weapon_anim.play("idle1"))
|
||||
|
||||
func _physics_process(dt: float) -> void:
|
||||
cooldown -= dt
|
||||
grenade_cooldown = maxf(0, grenade_cooldown - dt)
|
||||
flash.light_energy = maxf(0, flash.light_energy - dt * 60)
|
||||
gun.position.z = lerpf(gun.position.z, 0, dt * 15)
|
||||
if hp <= 0:
|
||||
return
|
||||
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
|
||||
var direction := transform.basis * Vector3(input.x, 0, input.y)
|
||||
velocity.x = direction.x * 5
|
||||
velocity.z = direction.z * 5
|
||||
velocity.y -= 20 * dt
|
||||
if is_on_floor() and Input.is_action_just_pressed("jump"):
|
||||
velocity.y = 7
|
||||
move_and_slide()
|
||||
step_timer -= dt
|
||||
if is_on_floor() and direction.length() > 0.1 and step_timer <= 0:
|
||||
step_timer = 0.42
|
||||
Audio.play(self, "player/pl_metal%d.wav" % randi_range(1, 4), false, -18)
|
||||
if Input.is_action_just_pressed("shoot") and ammo <= 0 and not reloading:
|
||||
Audio.play(self, "weapons/dryfire1.wav")
|
||||
if Input.is_action_pressed("shoot") and cooldown <= 0 and ammo > 0 and not reloading and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
shoot()
|
||||
|
||||
func shoot() -> void:
|
||||
if ammo <= 0 or reloading or hp <= 0:
|
||||
return
|
||||
weapon_anim.stop()
|
||||
weapon_anim.play("shoot")
|
||||
Audio.play(self, "weapons/hks%d.wav" % randi_range(1, 3), false, -13)
|
||||
ammo -= 1
|
||||
cooldown = 0.13
|
||||
flash.light_energy = 3
|
||||
gun.position.z = 0.06
|
||||
var start := camera.global_position
|
||||
var query := PhysicsRayQueryParameters3D.create(start, start - camera.global_basis.z * 80, 1 | 4)
|
||||
var hit := get_world_3d().direct_space_state.intersect_ray(query)
|
||||
if hit and hit.collider.has_method("take_damage"):
|
||||
Blood.impact(get_parent(), hit.position, -camera.global_basis.z)
|
||||
hit.collider.take_damage(35, hit.position.y)
|
||||
|
||||
func throw_grenade() -> RigidBody3D:
|
||||
if death_started or hp <= 0 or grenade_cooldown > 0:
|
||||
return null
|
||||
var world := get_parent() as Node3D
|
||||
var forward := -camera.global_basis.z
|
||||
var start := camera.global_position
|
||||
var destination := start + forward * 0.55
|
||||
# Do not spawn through the wall when the camera is close to it.
|
||||
var query := PhysicsRayQueryParameters3D.create(start, destination, 1)
|
||||
var hit := get_world_3d().direct_space_state.intersect_ray(query)
|
||||
if not hit.is_empty():
|
||||
destination = start + forward * maxf(0, start.distance_to(hit.position) - 0.16)
|
||||
var grenade := RigidBody3D.new()
|
||||
grenade.set_script(Grenade)
|
||||
grenade.thrower = self
|
||||
grenade.position = world.to_local(destination)
|
||||
world.add_child(grenade)
|
||||
grenade.linear_velocity = forward * 12 + Vector3.UP * 3 + velocity
|
||||
grenade.angular_velocity = Vector3(5, 3, 2)
|
||||
grenade_cooldown = GRENADE_COOLDOWN
|
||||
return grenade
|
||||
|
||||
func take_damage(amount: float) -> void:
|
||||
if death_started or amount <= 0:
|
||||
return
|
||||
hp = maxf(0, hp - amount)
|
||||
if hp <= 0:
|
||||
_die()
|
||||
else:
|
||||
damage_feedback.hit(amount)
|
||||
|
||||
func _die() -> void:
|
||||
death_started = true
|
||||
damage_feedback.stop()
|
||||
velocity = Vector3.ZERO
|
||||
reloading = false
|
||||
gun.hide()
|
||||
flash.light_energy = 0
|
||||
weapon_anim.stop()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
# Original Half-Life player pain sample used as the death event vocal.
|
||||
Audio.play(self, "player/pl_pain7.wav", false, -8)
|
||||
var layer := CanvasLayer.new()
|
||||
layer.layer = 5
|
||||
add_child(layer)
|
||||
death_overlay = ColorRect.new()
|
||||
death_overlay.color = Color(0, 0, 0, 0)
|
||||
death_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
layer.add_child(death_overlay)
|
||||
death_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
var tween := create_tween()
|
||||
tween.set_parallel(true)
|
||||
tween.tween_property(camera, "position:y", 0.22, 0.9).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
tween.tween_property(camera, "rotation:z", deg_to_rad(-75), 0.9)
|
||||
tween.tween_property(camera, "rotation:x", 0.08, 0.9)
|
||||
tween.tween_property(death_overlay, "color:a", 1.0, 1.8).set_delay(0.7)
|
||||
var message := Label.new()
|
||||
message.text = "SEI MORTO\nPremi R per ricominciare"
|
||||
message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
message.add_theme_font_size_override("font_size", 36)
|
||||
message.modulate.a = 0
|
||||
layer.add_child(message)
|
||||
message.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||
tween.tween_property(message, "modulate:a", 1.0, 0.5).set_delay(2.2)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfflxt06enx5
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
## Stanza 18x18x5 con 4 finestre (una per muro) e nessuna porta.
|
||||
## Geometria costruita in codice per determinismo; vetro = Area3D rompibile.
|
||||
class_name Room
|
||||
extends Node3D
|
||||
|
||||
const W: float = 18.0 # larghezza (x)
|
||||
const D: float = 18.0 # profondita (z)
|
||||
const H: float = 5.0 # altezza (y)
|
||||
const WALL_T: float = 0.3
|
||||
|
||||
# Finestra: 4 m x 2 m, bordo basso a 1.5 m
|
||||
const WIN_W: float = 4.0
|
||||
const WIN_H: float = 2.0
|
||||
const WIN_Y0: float = 1.5
|
||||
|
||||
var glass_panes: Array[Area3D] = []
|
||||
## Punti di spawn esterni, uno per finestra (4 finestre x 4 slot)
|
||||
var window_spawn_points: Array[Vector3] = []
|
||||
var window_normals: Array[Vector3] = [] # direzione "dentro" la stanza per ogni finestra
|
||||
|
||||
func _ready() -> void:
|
||||
var wall_mat := _surface_material("wall", 0.9)
|
||||
var floor_mat := _surface_material("floor", 0.95)
|
||||
var ceil_mat := _surface_material("ceiling", 0.85)
|
||||
|
||||
# Pavimento
|
||||
_box(Vector3(W, 0.2, D), Vector3(0, -0.1, 0), floor_mat, true)
|
||||
# Soffitto
|
||||
_box(Vector3(W, 0.2, D), Vector3(0, H + 0.1, 0), ceil_mat, false)
|
||||
|
||||
# 4 muri con apertura finestra al centro
|
||||
# nord (z = -D/2), sud (z = +D/2), ovest (x = -W/2), est (x = +W/2)
|
||||
var walls := [
|
||||
{"center": Vector3(0, H / 2, -D / 2 - WALL_T / 2), "size": Vector3(W, H, WALL_T), "axis": "x"},
|
||||
{"center": Vector3(0, H / 2, D / 2 + WALL_T / 2), "size": Vector3(W, H, WALL_T), "axis": "x"},
|
||||
{"center": Vector3(-W / 2 - WALL_T / 2, H / 2, 0), "size": Vector3(WALL_T, H, D), "axis": "z"},
|
||||
{"center": Vector3(W / 2 + WALL_T / 2, H / 2, 0), "size": Vector3(WALL_T, H, D), "axis": "z"},
|
||||
]
|
||||
for w in walls:
|
||||
_build_wall_with_window(w.center, w.size, w.axis, wall_mat)
|
||||
|
||||
# Vetrata per ogni finestra
|
||||
_add_glass(Vector3(0, WIN_Y0 + WIN_H / 2, -D / 2), Vector3(WIN_W, WIN_H, 0.05), Vector3(1, 0, 0))
|
||||
_add_glass(Vector3(0, WIN_Y0 + WIN_H / 2, D / 2), Vector3(WIN_W, WIN_H, 0.05), Vector3(1, 0, 0))
|
||||
_add_glass(Vector3(-W / 2, WIN_Y0 + WIN_H / 2, 0), Vector3(0.05, WIN_H, WIN_W), Vector3(0, 0, 1))
|
||||
_add_glass(Vector3(W / 2, WIN_Y0 + WIN_H / 2, 0), Vector3(0.05, WIN_H, WIN_W), Vector3(0, 0, 1))
|
||||
|
||||
# Punti di spawn: 4 slot per finestra, fuori dal muro
|
||||
var win_centers := [
|
||||
{"c": Vector3(0, 0, -D / 2), "n": Vector3(0, 0, 1)},
|
||||
{"c": Vector3(0, 0, D / 2), "n": Vector3(0, 0, -1)},
|
||||
{"c": Vector3(-W / 2, 0, 0), "n": Vector3(1, 0, 0)},
|
||||
{"c": Vector3(W / 2, 0, 0), "n": Vector3(-1, 0, 0)},
|
||||
]
|
||||
for wc in win_centers:
|
||||
var side: Vector3 = wc.n.cross(Vector3(0, 1, 0)) # asse laterale della finestra
|
||||
for i in 4:
|
||||
var off := (i - 1.5) * (WIN_W / 3.0)
|
||||
var p: Vector3 = wc.c + side * off + wc.n * (-1.2) # 1.2 m fuori dalla finestra
|
||||
window_spawn_points.append(p)
|
||||
window_normals.append(wc.n)
|
||||
|
||||
func _build_wall_with_window(center: Vector3, size: Vector3, axis: String, mat: StandardMaterial3D) -> void:
|
||||
# Segments around the window opening (window is centered on the wall face)
|
||||
var wall := StaticBody3D.new()
|
||||
wall.name = "Wall"
|
||||
add_child(wall)
|
||||
var half_win_x := WIN_W / 2.0
|
||||
var y0 := WIN_Y0
|
||||
var y1 := WIN_Y0 + WIN_H
|
||||
|
||||
if axis == "x": # muro lungo x
|
||||
var L := size.x
|
||||
var segs := [
|
||||
{"s": Vector3(L / 2 - half_win_x, size.y, size.z), "c": Vector3(-half_win_x - (L / 2 - half_win_x) / 2.0, 0, 0)},
|
||||
{"s": Vector3(L / 2 - half_win_x, size.y, size.z), "c": Vector3(half_win_x + (L / 2 - half_win_x) / 2.0, 0, 0)},
|
||||
{"s": Vector3(WIN_W, y0, size.z), "c": Vector3(0, -size.y / 2 + y0 / 2.0, 0)},
|
||||
{"s": Vector3(WIN_W, size.y - y1, size.z), "c": Vector3(0, size.y / 2 - (size.y - y1) / 2.0, 0)},
|
||||
]
|
||||
for seg in segs:
|
||||
_add_box_to(wall, seg.s, center + seg.c, mat)
|
||||
else: # muro lungo z
|
||||
var L := size.z
|
||||
var segs := [
|
||||
{"s": Vector3(size.x, size.y, L / 2 - half_win_x), "c": Vector3(0, 0, -half_win_x - (L / 2 - half_win_x) / 2.0)},
|
||||
{"s": Vector3(size.x, size.y, L / 2 - half_win_x), "c": Vector3(0, 0, half_win_x + (L / 2 - half_win_x) / 2.0)},
|
||||
{"s": Vector3(size.x, y0, WIN_W), "c": Vector3(0, -size.y / 2 + y0 / 2.0, 0)},
|
||||
{"s": Vector3(size.x, size.y - y1, WIN_W), "c": Vector3(0, size.y / 2 - (size.y - y1) / 2.0, 0)},
|
||||
]
|
||||
for seg in segs:
|
||||
_add_box_to(wall, seg.s, center + seg.c, mat)
|
||||
|
||||
func _add_glass(center: Vector3, size: Vector3, rot_axis: Vector3) -> void:
|
||||
var glass := Area3D.new()
|
||||
glass.name = "Glass"
|
||||
glass.collision_layer = 8
|
||||
glass.collision_mask = 0
|
||||
add_child(glass)
|
||||
var shape := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = size
|
||||
shape.shape = box
|
||||
glass.add_child(shape)
|
||||
glass.global_position = center
|
||||
var mesh := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = size
|
||||
var gm := StandardMaterial3D.new()
|
||||
gm.albedo_color = Color(0.7, 0.85, 0.9, 0.16)
|
||||
gm.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
gm.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
gm.shading_mode = BaseMaterial3D.SHADING_MODE_PER_PIXEL
|
||||
gm.roughness = 0.05
|
||||
gm.metallic = 0.1
|
||||
bm.material = gm
|
||||
mesh.mesh = bm
|
||||
glass.add_child(mesh)
|
||||
glass_panes.append(glass)
|
||||
|
||||
func shatter_glass(glass: Area3D) -> void:
|
||||
var sound_origin := Node3D.new()
|
||||
add_child(sound_origin)
|
||||
sound_origin.global_position = glass.global_position
|
||||
preload("res://scripts/hl_audio.gd").play(sound_origin, "debris/bustglass%d.wav" % randi_range(1, 3), true, -12)
|
||||
get_tree().create_timer(4).timeout.connect(sound_origin.queue_free)
|
||||
for i in glass_panes.size():
|
||||
if glass_panes[i] == glass:
|
||||
glass_panes.remove_at(i)
|
||||
break
|
||||
# Schegge di vetro (particelle semplici)
|
||||
var sh := CPUParticles3D.new()
|
||||
sh.direction = Vector3(0, -0.3, 0)
|
||||
sh.spread = 180
|
||||
sh.initial_velocity_min = 1.5
|
||||
sh.initial_velocity_max = 4
|
||||
sh.gravity = Vector3(0, -9.8, 0)
|
||||
sh.scale_amount_min = 0.4
|
||||
sh.scale_amount_max = 1.2
|
||||
sh.color = Color(0.7, 0.85, 1.0, 0.8)
|
||||
sh.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE
|
||||
sh.emission_sphere_radius = 1.2
|
||||
var shard := BoxMesh.new()
|
||||
shard.size = Vector3(0.05, 0.02, 0.08)
|
||||
var shard_mat := StandardMaterial3D.new()
|
||||
shard_mat.albedo_color = Color(0.6, 0.8, 0.9)
|
||||
shard.material = shard_mat
|
||||
sh.mesh = shard
|
||||
sh.amount = 40
|
||||
sh.lifetime = 0.9
|
||||
sh.one_shot = true
|
||||
sh.explosiveness = 1.0
|
||||
sh.position = glass.global_position
|
||||
add_child(sh)
|
||||
sh.emitting = true
|
||||
get_tree().create_timer(2).timeout.connect(sh.queue_free)
|
||||
glass.queue_free()
|
||||
|
||||
func _box(s: Vector3, c: Vector3, m: StandardMaterial3D, collide: bool) -> void:
|
||||
var body := StaticBody3D.new()
|
||||
add_child(body)
|
||||
_add_box_to(body, s, c, m)
|
||||
if not collide:
|
||||
body.get_node("shape").set_deferred("disabled", true)
|
||||
|
||||
func _add_box_to(parent: Node3D, s: Vector3, c: Vector3, m: StandardMaterial3D) -> void:
|
||||
var mi := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = s
|
||||
bm.material = m
|
||||
mi.mesh = bm
|
||||
parent.add_child(mi)
|
||||
var cs := CollisionShape3D.new()
|
||||
var bs := BoxShape3D.new()
|
||||
bs.size = s
|
||||
cs.shape = bs
|
||||
cs.position = c - parent.position if parent is StaticBody3D else c
|
||||
cs.name = "shape"
|
||||
parent.add_child(cs)
|
||||
# il mesh va centrato sullo stesso punto della collision
|
||||
mi.position = c - parent.position if parent is StaticBody3D else c
|
||||
|
||||
func _surface_material(texture_name: String, rough: float) -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
material.albedo_texture = load("res://assets/textures/" + texture_name + ".png")
|
||||
material.albedo_color = Color.WHITE
|
||||
material.roughness = rough
|
||||
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
|
||||
material.texture_repeat = true
|
||||
material.uv1_triplanar = true
|
||||
material.uv1_world_triplanar = true
|
||||
material.uv1_scale = Vector3.ONE * 0.5
|
||||
return material
|
||||
|
||||
func _mat(color: Color, rough: float) -> StandardMaterial3D:
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = color
|
||||
m.roughness = rough
|
||||
return m
|
||||
@@ -0,0 +1 @@
|
||||
uid://biqsa3jp3253l
|
||||
@@ -0,0 +1,93 @@
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://jriohka6tdgj
|
||||
Reference in New Issue
Block a user