extends CharacterBody3D var npc : bool = true @export var waypoint_route : Node3D = null var waypoints : Array var walk_speed : float = 2.0 var run_speed : float = 5.0 var time_suspicionbuild : float = 3.0 var time_suspiciondecay : float = 2.0 var time_cooldown : float = 2.0 # var ragdolling : bool = false var seeing_player : bool = false var move_this_frame : bool = false var unpause_this_frame : bool = false var on_route : bool = false @export var behavior_type : String = "guard" var state : String = "default" var current_waypoint : int = 0 var paused : bool = false var pauseanim : String var pauseturn : bool var pauseangle : float var lastseenpos : Vector3 var pathupdateinterval : float = 0.25 var player : Node @export var appearance : String = "guard" var current_path : PackedVector3Array @onready var default_3d_map_rid : RID = get_world_3d().get_navigation_map() var current_path_index : int = 0 var current_path_point : Vector3 var path_end_point : Vector3 @onready var overheadstatus : Control = $StatusSprite/SubViewport/OverheadStatus func _ready() -> void: overheadstatus.set_nothing() overheadstatus.set_timer(-1) call_deferred("init_route") $Timers/WaypointPause.timeout.connect(timeout_waypointpause) $Timers/Suspicion.timeout.connect(timeout_suspicion) $Timers/Sight.timeout.connect(timeout_sight) if appearance == "guard": $CharacterArmature/Skeleton3D/head_guard.visible = true $CharacterArmature/Skeleton3D/body_guard.visible = true $CharacterArmature/Skeleton3D/feet_guard.visible = true $CharacterArmature/Skeleton3D/legs_guard.visible = true elif appearance == "barista": $CharacterArmature/Skeleton3D/head_barista.visible = true $CharacterArmature/Skeleton3D/body_barista.visible = true $CharacterArmature/Skeleton3D/feet_barista.visible = true $CharacterArmature/Skeleton3D/legs_barista.visible = true func _physics_process(delta: float) -> void: seeing_player = checksight() ai_process() if move_this_frame: movement_process(delta) move_and_slide() animate() func impact(): # ragdolling = true state = "unconscious" set_collision_layer_value(1,0) # $CharacterArmature/Skeleton3D/ragdoll.active = true # $CharacterArmature/Skeleton3D/ragdoll.physical_bones_start_simulation() $AnimationPlayer.play("Death") stand_still() clearpath() func ai_process(): if unpause_this_frame: paused = false unpause_this_frame = false if state == "to_default": state = "default" elif state == "to_cooldown": clearpath() state = "cooldown" $Timers/Suspicion.stop() $Timers/Suspicion.start(time_cooldown) elif state == "unconscious": on_route = false $Timers/Suspicion.stop() $Timers/Sight.stop() overheadstatus.set_nothing() overheadstatus.set_timer(-1) if behavior_type == "guard": if state == "suspicion_escalatecheck": on_route = false $Timers/Suspicion.stop() $Timers/Sight.stop() if seeing_player: setpath(lastseenpos) overheadstatus.set_exclamationpoint() overheadstatus.set_timer(-1) $Timers/Sight.start(pathupdateinterval) state = "pursuit" else: state = "default" elif state == "pursuit_updatepath": $Timers/Sight.stop() $Timers/Sight.start(pathupdateinterval) setpath(lastseenpos) state = "pursuit" elif state == "cooldown_end": $Timers/Suspicion.stop() $Timers/Sight.stop() on_route = true if len(waypoints) > 1: current_waypoint = nearest_waypoint() state = "default" if state == "default": overheadstatus.set_nothing() overheadstatus.set_timer(-1) if paused: $AnimationPlayer.play(pauseanim) if pauseturn == true: lerp_toward(pauseangle) if seeing_player: $Timers/Suspicion.stop() $Timers/Suspicion.start(time_suspicionbuild) state = "suspicion" if paused: unpause_this_frame = true $Timers/WaypointPause.stop() else: on_route = true move_this_frame = true elif state == "suspicion": clearpath() on_route = false overheadstatus.set_questionmark() var timerpercent : float = ((time_suspicionbuild - $Timers/Suspicion.time_left) / time_suspicionbuild) * 100 overheadstatus.set_timer(timerpercent) lerp_toward(angle_toward(lastseenpos)) if seeing_player: if $Timers/Suspicion.paused: $Timers/Suspicion.paused = false if !$Timers/Sight.is_stopped(): $Timers/Sight.stop() $Timers/Sight.start(time_suspiciondecay) elif !seeing_player: if !$Timers/Suspicion.paused: $Timers/Suspicion.paused = true if $Timers/Sight.is_stopped(): $Timers/Sight.start(time_suspiciondecay) elif state == "pursuit": if global_transform.origin.distance_to(player.global_transform.origin) <= 0.8: $Timers/Sight.stop() state = "to_cooldown" elif global_transform.origin.distance_to(path_end_point) <= 0.5: $Timers/Sight.stop() pauseangle = $CharacterArmature.global_rotation.y - deg_to_rad(90) clearpath() state = "pursuit_lookleft" else: on_route = false if !current_path.is_empty(): move_this_frame = true elif state == "pursuit_lookleft": on_route = false if seeing_player: state = "suspicion_escalatecheck" else: lerp_toward(pauseangle, 0.05) var snappedangle = snapped(fposmod($CharacterArmature.global_rotation.y, TAU), 0.01) if snappedangle == snapped(fposmod(pauseangle, TAU), 0.01): pauseangle = $CharacterArmature.global_rotation.y + deg_to_rad(180) state = "pursuit_lookright" elif state == "pursuit_lookright": on_route = false if seeing_player: state = "suspicion_escalatecheck" else: lerp_toward(pauseangle, 0.05) var snappedangle = snapped(fposmod($CharacterArmature.global_rotation.y, TAU), 0.01) if snappedangle == snapped(fposmod(pauseangle, TAU), 0.01): pauseangle = $CharacterArmature.global_rotation.y + deg_to_rad(90) state = "pursuit_lookback" elif state == "pursuit_lookback": on_route = false if seeing_player: state = "suspicion_escalatecheck" else: lerp_toward(pauseangle, 0.05) var snappedangle = snapped(fposmod($CharacterArmature.global_rotation.y, TAU), 0.01) if snappedangle == snapped(fposmod(pauseangle, TAU), 0.01): state = "to_cooldown" elif state == "cooldown": overheadstatus.set_3dots() overheadstatus.subtractive = true var percent : float = ($Timers/Suspicion.time_left / time_suspiciondecay) * 100 overheadstatus.set_timer(percent) func movement_process(delta): if not is_on_floor(): velocity += get_gravity() * delta if on_route and len(waypoints) == 1 and (global_transform.origin.distance_to(waypoints[0].global_transform.origin) < 0.6): if $CharacterArmature.global_rotation.y != waypoints[0].global_rotation.y: lerp_toward(waypoints[0].global_rotation.y) stand_still() elif !current_path.is_empty(): current_path_point = current_path[current_path_index] if !paused: walk_toward(current_path_point) if global_transform.origin.distance_to(current_path_point) <= 0.25: current_path_index += 1 if current_path_index >= current_path.size(): if on_route: var wayedpoint = waypoints[current_waypoint] if wayedpoint.pausetime > 0: paused = true $Timers/WaypointPause.start(wayedpoint.pausetime) pauseanim = wayedpoint.animation if wayedpoint.turntoward == true: pauseturn = true pauseangle = wayedpoint.global_rotation.y else: pauseturn = false current_waypoint += 1 if current_waypoint > len(waypoints)-1: current_waypoint = 0 nextwaypoint() else: clearpath() elif NavigationServer3D.map_get_iteration_id(default_3d_map_rid) != 0 and current_path.is_empty() and on_route: nextwaypoint() else: stand_still() move_this_frame = false func animate(): if state == "unconscious": pass elif velocity.length() > 0: $AnimationPlayer.play("Walk") elif !paused or state != "default": $AnimationPlayer.play("Idle_Neutral") func angle_toward(target : Vector3): return (Vector2(target.z, target.x) - Vector2($CharacterArmature.global_transform.origin.z, $CharacterArmature.global_transform.origin.x)).angle() func walk_toward(point: Vector3): var direction = global_transform.origin.direction_to(point) velocity.x = direction.x * walk_speed velocity.z = direction.z * walk_speed print(direction) lerp_toward(atan2(velocity.x, velocity.z)) func stand_still(): velocity.x = 0 velocity.z = 0 func nextwaypoint(): var waypoint = waypoints[current_waypoint] var target_position: Vector3 = waypoint.global_transform.origin setpath(target_position) func setpath(target_position): clearpath() var start_position: Vector3 = global_transform.origin current_path = NavigationServer3D.map_get_path( default_3d_map_rid, start_position, target_position, true ) if len(current_path) > 0: path_end_point = current_path[len(current_path)-1] func clearpath(): current_path = [] current_path_index = 0 current_path_point = global_transform.origin velocity.x = 0 velocity.z = 0 func checksight(): var seenthings = $CharacterArmature/SightCollider.get_overlapping_bodies() if !seenthings.is_empty(): for item in seenthings: if item.get_name() == "PlayerVisibilityShape": player = item.get_parent().get_parent() if sightraycheck(item) == true: return true return false func sightraycheck(target): # var radius = target.shape_owner_get_shape(0,0).radius var height : float var tramsorm : Transform3D if target.get_parent().get_parent().get_name() == "Player" and player.crouched: height = target.shape_owner_get_shape(1,0).height tramsorm = target.shape_owner_get_transform(1).rotated(Vector3(0,1,0),target.global_rotation.y) else: height = target.shape_owner_get_shape(0,0).height tramsorm = target.shape_owner_get_transform(0).rotated(Vector3(0,1,0),target.global_rotation.y) var top = to_local(target.global_transform.origin - $CharacterArmature/SightCollider/SightRay.transform.origin + tramsorm.origin + Vector3(0, (height/2)-0.1, 0)) $CharacterArmature/SightCollider/SightRay.target_position = top $CharacterArmature/SightCollider/SightRay.global_rotation = global_rotation $CharacterArmature/SightCollider/SightRay.force_raycast_update() if $CharacterArmature/SightCollider/SightRay.is_colliding(): if $CharacterArmature/SightCollider/SightRay.get_collider().get_name() == "PlayerVisibilityShape": lastseenpos = target.global_transform.origin return true return false func lerp_toward(angle: float, speed = 0.15): $CharacterArmature.global_rotation.y = lerp_angle($CharacterArmature.global_rotation.y, angle, speed) func get_waypoints(path: Node3D): for point in path.get_children(): waypoints.append(point) func nearest_waypoint(): var nearest : Vector3 = waypoints[0].global_transform.origin var nearest_index : int = 0 for point in len(waypoints): if global_transform.origin.distance_to(waypoints[point].global_transform.origin) < global_transform.origin.distance_to(nearest): nearest = waypoints[point].global_transform.origin nearest_index = point return nearest_index func init_route() -> void: if waypoint_route == null: var startpos = Marker3D.new() get_tree().current_scene.add_child(startpos) startpos.set_global_transform(global_transform) startpos.set_script(preload("res://scripts/waypoint.gd")) waypoints.append(startpos) else: get_waypoints(waypoint_route) func timeout_waypointpause() -> void: if paused: unpause_this_frame = true func timeout_suspicion() -> void: if behavior_type == "guard": if state == "suspicion": state = "suspicion_escalatecheck" if state == "pursuit_lookaround": state = "to_cooldown" elif state == "cooldown": state = "cooldown_end" func timeout_sight() -> void: if behavior_type == "guard": if state == "suspicion": $Timers/Sight.stop() state = "to_default" elif state == "pursuit": $Timers/Sight.stop() $Timers/Sight.start(pathupdateinterval) if path_end_point.distance_to(lastseenpos) >= 2: state = "pursuit_updatepath"