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() var dummy : Spatial var in_detection_normal = false var in_detection_focused = false 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 = $merc1/CharacterRig001 onready var shoot_from = player_model.get_node(@"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 reaction_time = $ReactionTime.get_wait_time() onready var sight_normal = $"PlayerDetection1/NormalLoS" onready var sight_focused = $"PlayerDetection1/AttentiveLoS" func _ready(): # Pre-initialize orientation transform. orientation = player_model.global_transform orientation.origin = Vector3() dummy = Spatial.new() 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 $ReactionTime.time_left == 0: # 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) elif angle_to_player < -tolerance: lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED) 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 = player_model.get_node(@"Skeleton/GunBone/ShootFrom/ShootParticle") shoot_particle.restart() shoot_particle.emitting = true var muzzle_particle = player_model.get_node(@"Skeleton/GunBone/ShootFrom/MuzzleFlash") muzzle_particle.restart() muzzle_particle.emitting = true fire_cooldown.start() sound_effect_shoot.play() else: if $ReactionTime.is_paused(): $ReactionTime.set_paused(false) #restart reaction time else: # Player not in sight. shoot_countdown = SHOOT_WAIT if $ReactionTime.time_left != reaction_time: $ReactionTime.start() $ReactionTime.set_paused(true) # 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) elif angle_to_player < -tolerance: lerp_to_face_player(angle_to_player, ROTATION_INTERPOLATE_SPEED) 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 = animation_tree.get_root_motion_transform() velocity = move_and_slide(root_motion.origin.length() * self.transform.basis.z * Vector3(1,0,1), Vector3.UP) self.global_transform.origin += velocity #self.global_transform.origin += root_motion.origin.length() * self.transform.basis.z * Vector3(1,0,1) 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() # If shot at, attempt to look for player. This will extrapolate from impact position # so they may validly look the wrong direction if player == null or player == dummy: player = dummy last_player_sighting = (col.position - self.global_transform.origin) * 20 + self.global_transform.origin # Enable longer focus detection shape sight_focused.disabled = false func _on_PlayerDetection1_body_entered(body): if body.name == "Player" or body.name == "Target": in_detection_normal = true player = body # Enable longer focus detection shape sight_focused.disabled = false func _on_PlayerDetection1_body_exited(body): if body.name == "Player": player = null sight_focused.disabled = true