extends CharacterBody3D var camera: Camera3D var damage_pivot: Node3D var damage_feedback: CanvasLayer const DamageFeedback = preload("res://scripts/damage_feedback.gd") const MAX_HP := 100.0 var hp := MAX_HP var _regen_elapsed := 0.0 ## Discreto: +1 HP per secondo di gioco, mai oltre il massimo o dopo morte. func _regenerate_health(dt: float) -> void: if hp <= 0 or death_started or hp >= MAX_HP: _regen_elapsed = 0.0 return _regen_elapsed += dt while _regen_elapsed >= 1.0: _regen_elapsed -= 1.0 hp = minf(MAX_HP, hp + 1.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 ## True quando la partita e' guidata dai controlli on-screen (touch): il mouse ## emulato dai tocchi non deve muovere la visuale ne' sparare al posto del dito. var touch_mode := false 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 var restart_button: Button func _input(event: InputEvent) -> void: # Touch nativo: funziona anche con emulate_mouse_from_touch disattivato. if event is InputEventScreenTouch and event.pressed and is_instance_valid(restart_button): if not restart_button.disabled and restart_button.get_global_rect().has_point(event.position): restart_button.disabled = true get_viewport().set_input_as_handled() get_tree().call_deferred("reload_current_scene") func _unhandled_input(event: InputEvent) -> void: if OS.has_feature("web") and not touch_mode 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 not touch_mode and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: apply_look(event.relative) 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 ## Ricarica: tempi invariati, riusata da tastiera (R) e dal pulsante on-screen. func start_reload() -> void: if reloading or death_started or hp <= 0 or ammo >= 30: return 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")) ## Azioni a pressione singola lette a polling: stesso percorso per tastiera, ## joypad e pulsanti on-screen. Gli InputEventAction sintetici, generati dentro ## il dispatch di un altro evento (come fa un tocco), non arrivano affidabilmente ## a _unhandled_input; lo stato delle azioni invece si'. func _handle_actions() -> void: if Input.is_action_just_pressed("reload") and not reloading and ammo < 30: start_reload() if Input.is_action_just_pressed("grenade") and not death_started and (touch_mode or Input.mouse_mode == Input.MOUSE_MODE_CAPTURED): throw_grenade() ## Rotazione della visuale. Unico punto in cui vivono sensibilita' e limiti di ## inclinazione, condiviso da mouse e controlli touch. func apply_look(relative: Vector2) -> void: if hp <= 0 or death_started: return rotate_y(-relative.x * 0.0025) camera.rotation.x = clampf(camera.rotation.x - relative.y * 0.0025, -1.4, 1.4) 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: if Input.is_action_just_pressed("reload"): get_tree().reload_current_scene() # R o pulsante Ricarica a partita finita return _regenerate_health(dt) if ammo <= 0 and not reloading: start_reload() _handle_actions() # Deadzone esplicita piccola (0.2): con la 0.5 delle azioni la corsa utile del # joystick touch si dimezzerebbe, mentre per tastiera/joypad nulla cambia. var input := Input.get_vector("move_left", "move_right", "move_forward", "move_back", 0.2) 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 (touch_mode or 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) if ammo <= 0: start_reload() 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) Audio.play(self, "fvox/flatline.wav", false, -9) 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" if touch_mode else "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) if touch_mode: var restart := Button.new() restart_button = restart restart.name = "RestartButton" restart.text = "RICOMINCIA" restart.add_theme_font_size_override("font_size", 28) layer.add_child(restart) restart.set_anchors_and_offsets_preset(Control.PRESET_CENTER) restart.offset_left = -150 restart.offset_right = 150 restart.offset_top = 90 restart.offset_bottom = 162 restart.disabled = true get_tree().create_timer(0.6).timeout.connect(func(): restart.disabled = false) restart.pressed.connect(func(): restart.disabled = true get_tree().reload_current_scene())