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,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)
|
||||
Reference in New Issue
Block a user