extends CharacterBody3D var walk_speed : float = 2.0 var run_speed : float = 5.0 @export var waypoint_route : Node3D var state_change_cooldown : float = 0.1 var alert_escalation_time : float = 3.0 var suspicion_decay_time : float = 1.0 @export var behavior_type : String = "frozen" var state : String = "patrol" var current_waypoint : int = 0 var paused : bool = false var turntoward : float = 0.0 var lastseenpos : Vector3 var pathupdateinterval : float = 0.75 @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 @onready var overheadstatus : Control = $StatusSprite/SubViewport/OverheadStatus func _ready() -> void: overheadstatus.set_nothing() overheadstatus.set_timer(-1) 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: if behavior_type == "frozen": sight_test() elif behavior_type == "guard": process_alerts() if not is_on_floor(): velocity += get_gravity() * delta if !current_path.is_empty() and behavior_type == "guard" and (state == "patrol" or state == "pursue_walkto"): current_path_point = current_path[current_path_index] walk_toward(current_path_point) if global_transform.origin.distance_to(current_path_point) <= 0.6: current_path_index += 1 if current_path_index >= current_path.size(): clearpath() if state == "patrol": var wayedpoint = waypoint_route.get_children()[current_waypoint] if wayedpoint.pausetime > 0: paused = true $WaypointPauseTimer.start(wayedpoint.pausetime) $AnimationPlayer.play(wayedpoint.animation) if wayedpoint.turntoward == true: turntoward = wayedpoint.global_rotation.y else: turntoward = $CharacterArmature.global_rotation.y current_waypoint += 1 if current_waypoint > waypoint_route.get_child_count()-1: current_waypoint = 0 if state == "pursue_walkto": print("done!") state = "suspicion_start" $StateChangeTimer.start(state_change_cooldown) print("player sighted! state changed to suspicion_start") overheadstatus.set_3dots() overheadstatus.set_timer(-1) else: stand_still() if paused and state == "patrol": # overheadstatus.set_timer(100-(($WaypointPauseTimer.time_left / $WaypointPauseTimer.wait_time)*100)) if $CharacterArmature.global_rotation.y != turntoward: lerp_toward(turntoward) move_and_slide() animate() func animate(): if velocity.length() > 0: $AnimationPlayer.play("Walk") elif !paused: $AnimationPlayer.play("Idle_Neutral") 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 lerp_toward(atan2(velocity.x, velocity.z)) func stand_still(): velocity.x = 0 velocity.z = 0 func sight_test(): var seeingplayer : bool seeingplayer = checksight() if seeingplayer: overheadstatus.set_exclamationmark() else: overheadstatus.set_nothing() func process_alerts(): var seeingplayer : bool if state == "patrol": seeingplayer = checksight() if seeingplayer: state = "suspicion_start" $StateChangeTimer.start(state_change_cooldown) print("player sighted! state changed to suspicion_start") return if state == "suspicion": seeingplayer = checksight() if !seeingplayer and $StateChangeTimer.is_stopped(): $StateChangeTimer.start(suspicion_decay_time) elif seeingplayer: if !$StateChangeTimer.is_stopped(): $StateChangeTimer.stop() overheadstatus.set_questionmark() var percent : float = 100 - (($AlertEscalationTimer.time_left / alert_escalation_time) * 100) overheadstatus.set_timer(percent) if velocity.length() == 0: lerp_toward(turntoward) if state == "pursue_walkto": seeingplayer = checksight() if seeingplayer: print("update path?") if !current_path.is_empty() and (current_path[current_path.size()-1].distance_to(lastseenpos) > 1): setpath(lastseenpos) print("updated path!") func nextwaypoint(): var waypoint = waypoint_route.get_children()[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 ) 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": 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().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 = Vector3(0,0,0) $CharacterArmature/SightCollider/SightRay.force_raycast_update() if $CharacterArmature/SightCollider/SightRay.is_colliding(): if $CharacterArmature/SightCollider/SightRay.get_collider().get_name() == "PlayerVisibilityShape": turntoward = (Vector2(target.global_transform.origin.z, target.global_transform.origin.x) - Vector2($CharacterArmature.global_transform.origin.z, $CharacterArmature.global_transform.origin.x)).angle() lastseenpos = target.global_transform.origin return true return false func lerp_toward(angle: float): $CharacterArmature.global_rotation.y = lerp_angle($CharacterArmature.global_rotation.y, angle, 0.15) func _on_state_change_timer_timeout() -> void: $StateChangeTimer.stop() $AlertEscalationTimer.stop() if state == "suspicion_start": state = "suspicion" $AlertEscalationTimer.start(alert_escalation_time) return if state == "suspicion": state = "patrol" overheadstatus.set_3dots() overheadstatus.set_timer(-1) return if state == "pursue_start": setpath(lastseenpos) state = "pursue_walkto" return func _on_alert_escalation_timer_timeout() -> void: $AlertEscalationTimer.stop() if state == "suspicion": state = "pursue_start" $StateChangeTimer.start(state_change_cooldown) overheadstatus.set_timer(-1) overheadstatus.set_exclamationmark() func _on_waypoint_pause_timer_timeout() -> void: $WaypointPauseTimer.stop() if paused: paused = false