extends Spatial class_name NPC export(Array, NodePath) var tasks : Array export var modelSize = 1.0 export var spriteSize = 1.0 export var spriteOffset = 0.0 export var spriteFPS = 15 export var timeline = "Test" var speed = 1.0 var taskNodes = [] var navAgent : NavigationAgent var npcModel = null # animated npc model var camera = null # player camera for sprite calculation var action = "Idle" # current animation var skipSprites = false # skips sprite calculation var spriteFrame = 0 var shouldNavigate = false # Does the current task require navigation? var isAggressive = false # should hunt player if he is spotted var investigateSounds = false # should investigate sounds # senses var sightNear : bool = false var sightFar : bool = false var hearNear : bool = false var hearFar : bool = false var has_heard = null var velocity = Vector3.ZERO var targetPos var direction var navigate = false var currentTask = null # The current task the NPC wants to do ( usually timed tasks ) var playerTask = null # when the. spotted player var investigate : Spatial # A point of interest to investigate ( usually when the player made a sound ) var task = null # the actual task func _ready(): $NPCSprite.texture = $NPCRenderer.get_texture() $NPCRenderer.set_update_mode(Viewport.UPDATE_ONCE) investigate = Spatial.new() get_tree().current_scene.call_deferred("add_child", investigate) navAgent = $NavigationAgent navAgent.connect("velocity_computed", self, "_on_velocity_computed") for i in tasks: taskNodes.push_back(get_node(i)) func SetModel(model): npcModel = model camera = get_viewport().get_camera() model.get_parent().remove_child(model) $NPCRenderer.add_child(model) model.scale_object_local(Vector3(modelSize, modelSize, modelSize)) $NPCSprite.scale_object_local(Vector3(spriteSize, spriteSize, spriteSize)) $NPCSprite.translate_object_local(Vector3(0, spriteOffset, 0)) $NPC_Name.name = name func changeState(state): action = state if npcModel != null: npcModel.setAnimation(state) func _process(_delta): if camera == null: return calcSprites() moveNPC() func _on_VisibilityEnabler_screen_entered(): skipSprites = false func _on_VisibilityEnabler_screen_exited(): skipSprites = true func calcSprites(): if skipSprites: pass var camPos = camera.global_transform.origin camPos.y = 0 $Look.global_transform = global_transform $Look.global_transform.origin.y = 0 $Look.look_at(camPos, Vector3.UP) $Look.rotation_degrees.y = stepify($Look.rotation_degrees.y, 45.0) $NPCRenderer/CameraRig.global_rotation = $Look.global_rotation if npcModel != null: npcModel.rotation = rotation spriteFrame += 1 if spriteFrame % spriteFPS == 0: spriteFrame = 0 $NPCRenderer.set_update_mode(Viewport.UPDATE_ONCE) func Tick(hour, minute, _day): for t in taskNodes: if t.hour == hour and t.minute == minute: currentTask = t shouldNavigate = t.shouldNavigate if t.changeAggression: isAggressive = t.aggression if t.changeInvestigation: investigateSounds = t.shouldInvestigate func _on_velocity_computed(new_velocity): if task == null or targetPos == null: return velocity = new_velocity global_translate(new_velocity) global_transform.origin.y = targetPos.y #$RayCast.get_collision_point().y look_at(targetPos, Vector3.UP) ########## ## SENSES ########## func _on_HearingNear_body_entered(body): if body.is_in_group("Player"): hearNear = true func _on_HearingNear_body_exited(body): if body.is_in_group("Player"): hearNear = false func _on_HearingFar_body_entered(body): if body.is_in_group("Player"): hearFar = true func _on_HearingFar_body_exited(body): if body.is_in_group("Player"): hearFar = false func _on_SightNear_body_entered(body): if body.is_in_group("Player"): sightNear = true func _on_SightNear_body_exited(body): if body.is_in_group("Player"): sightNear = false func _on_SightFar_body_entered(body): if body.is_in_group("Player"): sightFar = true func _on_SightFar_body_exited(body): if body.is_in_group("Player"): sightFar = false func canSeePlayer(lightLevel): if not sightFar and not sightNear: return false var space_state = get_world().direct_space_state var result = space_state.intersect_ray($Head.global_transform.origin, camera.global_transform.origin) if result.size() > 0: var col = result["collider"] if col.is_in_group("Player"): if col.lightLevel > lightLevel: ## Detected player playerTask = col return true if playerTask != null: investigate.global_transform = playerTask.global_transform playerTask = null return false ########### ## REACTION ########### func interact(_relate): ### TALKING TO NPC #if behavior == null: # return #behavior.interact(relate) pass func heard(something): if investigateSounds: has_heard = something func _on_InteractionDetection_body_entered(body): if body.name == "Door" and velocity.length() > 0 and body.state == 0: body.interact(self) func _on_Tick_timeout(): # Update AI investigate.global_translation != Vector3.ZERO if isAggressive: canSeePlayer(0.5) if has_heard != null: investigate.global_transform = has_heard.global_transform has_heard = null if playerTask != null: task = playerTask elif investigate.global_translation != Vector3.ZERO: task = investigate elif currentTask != null: task = currentTask else: task = null navigate = false if task == null: return if task.has_method("get_transform"): navAgent.set_target_location(task.get_transform().origin) else: navAgent.set_target_location(task.global_transform.origin) navigate = not reachedTask() func reachedTask(): if not task.has_method("waiting"): return _reachedTarget() #if task.should_align(): # global_transform = task.get_transform() if task.waiting(): return false if task.get_interaction() == null: return true if not task.get_interaction().has_method("interact"): return true task.get_interaction().interact(self) if task.done(): task = null currentTask = null return true else: navAgent.set_target_location(task.get_transform().origin) return false func _reachedTarget(): if navAgent.is_navigation_finished(): velocity = Vector3.ZERO navAgent.set_velocity(velocity) return true return false func moveNPC(): if not navigate: return if task == currentTask and not shouldNavigate: return targetPos = navAgent.get_next_location() targetPos.y = 0 direction = global_transform.origin.direction_to(targetPos) var v = direction * speed * get_process_delta_time() navAgent.set_velocity(v)