Files
Dawn-Godot/scripts/entities/NPC.gd
2025-07-01 12:15:25 -05:00

159 lines
3.9 KiB
GDScript

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()