class_name NPC extends CharacterBody3D const CONVERSATION_FADE_DURATION:float = 0.3 enum InteractType { NONE, TEXTBOX, CUTSCENE } # Movement speed in units per second @export var gravity: float = 24.8 @export var interactType:InteractType = InteractType.NONE @export_multiline var interactTexts:Array[String] = [] @export var npcLookAtPlayer:bool = true @export var playerLookAtNPC:bool = true @export var interactCamera:Camera3D = null @export var cutscene:Cutscene = null var nextTextIndex:int = 0 var previousCamera:Camera3D = null var playerEnt:Player var conversationEnding:bool = false func lookAtPlayer() -> void: if !playerEnt: return # Get player position and NPC position var playerPos = playerEnt.global_transform.origin var npcPos = global_transform.origin # Rotate the NPC to look at the player if npcLookAtPlayer: var npcDirection = (playerPos - npcPos).normalized() var npcRotation = Vector3(0, atan2(npcDirection.x, npcDirection.z), 0) global_transform.basis = Basis.from_euler(npcRotation) # Rotate the player to look at the NPC if playerLookAtNPC: var playerDirection = (npcPos - playerPos).normalized() var playerRotation = Vector3(0, atan2(playerDirection.x, playerDirection.z), 0) playerEnt.global_transform.basis = Basis.from_euler(playerRotation) func showTexts(): TRANSITION.fadeOutStart.disconnect(showTexts) TRANSITION.fadeInEnd.disconnect(showTexts) # Any texts? if interactTexts.size() == 0: endTexts() return # First text. UI.TEXTBOX.setText(interactTexts[nextTextIndex]) UI.TEXTBOX.textboxClosing.connect(onTextboxClosing) func endTexts(): UI.TEXTBOX.textboxClosing.disconnect(onTextboxClosing) conversationEnding = true # Do we fade out the camera? if interactCamera: TRANSITION.fade(TRANSITION.FadeType.FADE_OUT, CONVERSATION_FADE_DURATION) TRANSITION.fadeOutEnd.connect(onFadeOutEnd) return # Reset Camera? if previousCamera: previousCamera.current = true previousCamera = null PAUSE.cutsceneResume() func _enter_tree() -> void: $InteractableArea.interactEvent.connect(onInteract) func _exit_tree() -> void: $InteractableArea.interactEvent.disconnect(onInteract) UI.TEXTBOX.textboxClosing.disconnect(onTextboxClosing) TRANSITION.fadeOutEnd.disconnect(onFadeOutEnd) TRANSITION.fadeInEnd.disconnect(onFadeInEnd) func _physics_process(delta): # Apply gravity if not on floor if !is_on_floor(): velocity += PHYSICS.GRAVITY * delta move_and_slide() func onInteract(playerEntity:Player) -> void: # Reset state previousCamera = null nextTextIndex = 0 playerEnt = playerEntity conversationEnding = false match interactType: InteractType.TEXTBOX: PAUSE.cutscenePause() # If a camera is set, switch to it, otherwise chat immediately. if interactCamera == null: lookAtPlayer() showTexts() return # Fade out. TRANSITION.fade(TRANSITION.FadeType.FADE_OUT, CONVERSATION_FADE_DURATION) TRANSITION.fadeOutEnd.connect(onFadeOutEnd) InteractType.CUTSCENE: if !cutscene: return cutscene.start() _: return func onTextboxClosing() -> void: nextTextIndex += 1 # More text? if nextTextIndex < interactTexts.size(): UI.TEXTBOX.setText(interactTexts[nextTextIndex]) else: endTexts() func onFadeOutEnd() -> void: # Begin fade back in. TRANSITION.fadeOutEnd.disconnect(onFadeOutEnd) TRANSITION.fade(TRANSITION.FadeType.FADE_IN, CONVERSATION_FADE_DURATION) TRANSITION.fadeInEnd.connect(onFadeInEnd) # Is the conversation ending? if conversationEnding: # Reset camera. if previousCamera: previousCamera.current = true previousCamera = null return # No! We are starting the conversation, make the ents look at each other lookAtPlayer() # Change camera previousCamera = get_viewport().get_camera_3d() interactCamera.current = true func onFadeInEnd() -> void: TRANSITION.fadeInEnd.disconnect(onFadeInEnd) # Did the conversation end? if conversationEnding: PAUSE.cutsceneResume() return # Show texts after fade in. showTexts()