extends KinematicBody enum State { APPROACH = 0, AIM = 1, SHOOTING = 2, } const PLAYER_AIM_TOLERANCE_DEGREES = 15 const SHOOT_WAIT = 0.5 const AIM_TIME = 1 const AIM_PREPARE_TIME = 0.5 const BLEND_AIM_SPEED = 0.05 export(int) var health = 5 export(bool) var test_shoot = false var state = State.APPROACH var shoot_countdown = SHOOT_WAIT var aim_countdown = AIM_TIME var aim_preparing = AIM_PREPARE_TIME var dead = false var player = null var velocity = Vector3() var orientation = Transform() const ROTATION_INTERPOLATE_SPEED = 0.025 var root_motion = Transform() var motion = Vector2() var last_player_sighting = Vector3() onready var initial_position = transform.origin onready var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") * ProjectSettings.get_setting("physics/3d/default_gravity_vector") onready var animation_tree = $AnimationTree onready var player_model = $PlayerModel onready var shoot_from = player_model.get_node(@"Colette_Armature/Skeleton/GunBone/ShootFrom") onready var fire_cooldown = $FireCooldown onready var collision_shape = $CapsuleShape onready var sound_effects = $SoundEffects onready var sound_effect_jump = sound_effects.get_node(@"Jump") onready var sound_effect_land = sound_effects.get_node(@"Land") onready var sound_effect_shoot = sound_effects.get_node(@"Shoot") onready var explosion_sound = sound_effects.get_node(@"Explosion") onready var hit_sound = sound_effects.get_node(@"Hit") onready var debug_marker = $debug_marker func _ready(): # Pre-initialize orientation transform. orientation = player_model.global_transform orientation.origin = Vector3() func _physics_process(delta): if dead: return if player != null and fire_cooldown.time_left == 0: # See if player can be killed because in they're sight. var ray_origin = shoot_from.global_transform.origin var ray_to = player.global_transform.origin + Vector3.UP # Above middle of player. var col = get_world().direct_space_state.intersect_ray(ray_origin, ray_to, [self], 0b111) # If our raycast hit a player if not col.empty() and col.collider == player: # Record last sighting (in case they hide later) last_player_sighting = ray_to state = State.AIM aim_countdown = AIM_TIME aim_preparing = 0 animation_tree["parameters/state/current"] = 1 # If the player is detected by at too large an angle, slerp rotate towards them var to_player_local = global_transform.xform_inv(player.global_transform.origin) # The front of this is +Z, and atan2 is zero at +X, so we need to use the Z for the X parameter (second one). var angle_to_player = atan2(to_player_local.x, to_player_local.z) var tolerance = deg2rad(PLAYER_AIM_TOLERANCE_DEGREES) if angle_to_player > tolerance: lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED) debug_marker.global_transform.origin = self.global_transform.origin + (self.transform.basis.z * 3) elif angle_to_player < -tolerance: lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED) debug_marker.global_transform.origin = self.global_transform.origin + (self.transform.basis.z * 3) else: # Facing player, try to shoot. self.look_at(ray_to, Vector3.UP) self.rotate_object_local(Vector3.UP, PI) var bullet = preload("res://player/bullet/bullet.tscn").instance() get_parent().add_child(bullet) bullet.global_transform.origin = ray_origin # If we don't rotate the bullets there is no useful way to control the particles .. bullet.look_at(ray_to, Vector3.UP) bullet.add_collision_exception_with(self) var shoot_particle = $PlayerModel/Colette_Armature/Skeleton/GunBone/ShootFrom/ShootParticle shoot_particle.restart() shoot_particle.emitting = true var muzzle_particle = $PlayerModel/Colette_Armature/Skeleton/GunBone/ShootFrom/MuzzleFlash muzzle_particle.restart() muzzle_particle.emitting = true fire_cooldown.start() sound_effect_shoot.play() # Player not in sight. shoot_countdown = SHOOT_WAIT # Run at their last known position if last_player_sighting != Vector3(): # If the player is detected by at too large an angle, slerp rotate towards them var to_player_local = global_transform.xform_inv(last_player_sighting) # The front of this is +Z, and atan2 is zero at +X, so we need to use the Z for the X parameter (second one). var angle_to_player = atan2(to_player_local.x, to_player_local.z) var tolerance = deg2rad(PLAYER_AIM_TOLERANCE_DEGREES) if angle_to_player > tolerance: lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED) debug_marker.global_transform.origin = self.global_transform.origin + (self.transform.basis.z * 3) elif angle_to_player < -tolerance: lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED) debug_marker.global_transform.origin = self.global_transform.origin + (self.transform.basis.z * 3) animation_tree["parameters/state/current"] = 2 # Blend position for walk speed based on motion. # Run animation straight forward, high speed animation_tree["parameters/walk/blend_position"] = Vector2(0.5, 0) # move in XY direction facing root_motion = global_transform.basis.z * 3# * delta move_and_slide(root_motion.length() * self.transform.basis.z * Vector3(1,0,1), Vector3.UP) if get_slide_count() == 0: move_and_slide(Vector3.DOWN * 9.8, Vector3.UP) # else: # Not in air or aiming, idle. func lerp_to_face_player(angle_to_player, rot_speed): var langle = lerp_angle(0, angle_to_player, rot_speed) # restrict to Y axis (no vertical rotation) self.global_transform.basis = Basis(Vector3(0, 1, 0), langle) * self.global_transform.basis func hit(col): health -= 1 if health <= 0: dead = true animation_tree.active = false player_model.visible = false collision_shape.disabled = true explosion_sound.play() self.queue_free() # delete self hit_sound.play() func _on_Area_body_entered(body): if body.name == "Player" or body.name == "Target": player = body animation_tree["parameters/idle_aim/current"] = 1 func _on_Area_body_exited(body): if body.name == "Player": player = null animation_tree["parameters/idle_aim/current"] = 0