diff --git a/project.godot b/project.godot index c407b19..641d4ba 100644 --- a/project.godot +++ b/project.godot @@ -20,6 +20,7 @@ config/icon="res://icon.svg" PHYSICS="*res://scripts/singletons/GamePhysics.gd" UI="*res://scenes/singletons/UI.tscn" PAUSE="*res://scripts/singletons/Pause.gd" +TRANSITION="*res://scenes/singletons/Transition.tscn" [debug] diff --git a/scenes/TestScene.tscn b/scenes/TestScene.tscn index 8b0dba2..289793d 100644 --- a/scenes/TestScene.tscn +++ b/scenes/TestScene.tscn @@ -38,10 +38,11 @@ point_count = 2 [node name="Player" parent="." instance=ExtResource("2_rlkm5")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.06402, 1.54702, -2.35884) -[node name="NPC" parent="." node_paths=PackedStringArray("cutscene") instance=ExtResource("4_nb1wl")] +[node name="NPC" parent="." node_paths=PackedStringArray("interactCamera") instance=ExtResource("4_nb1wl")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4.20455, 1.94676, -3.51349) -interactType = 2 -cutscene = NodePath("../Cutscenes/TestCutscene") +interactType = 1 +interactTexts = Array[String](["test"]) +interactCamera = NodePath("../TestChatCamera") [node name="Camera3D" type="Camera3D" parent="." node_paths=PackedStringArray("targetFollow", "pathFollow", "pathMap")] transform = Transform3D(0.996991, 0.0418507, -0.0652558, 0, 0.841762, 0.539849, 0.0775229, -0.538225, 0.839228, 1.25757, 8.21861, 8.01254) @@ -52,6 +53,10 @@ pathFollow = NodePath("../PathCamera/PathFollow3D") pathMap = NodePath("../PathWorld") metadata/_custom_type_script = "uid://csb0i132lcu0w" +[node name="TestChatCamera" type="Camera3D" parent="."] +transform = Transform3D(0.854722, 0.231437, -0.464637, 0, 0.895106, 0.445854, 0.519087, -0.381081, 0.765066, -6.8472, 4.22457, 0.61366) +fov = 47.0 + [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = SubResource("Environment_m5dm6") diff --git a/scenes/entities/Player.tscn b/scenes/entities/Player.tscn index 33acd9c..a8c8ecb 100644 --- a/scenes/entities/Player.tscn +++ b/scenes/entities/Player.tscn @@ -18,7 +18,6 @@ size = Vector3(0.705444, 0.680542, 1.17688) [node name="Player" type="CharacterBody3D"] script = ExtResource("1_24gqh") -metadata/_custom_type_script = "uid://c0by5m1upv57h" [node name="Scripts" type="Node" parent="."] @@ -26,9 +25,10 @@ metadata/_custom_type_script = "uid://c0by5m1upv57h" script = ExtResource("2_o7et6") player = NodePath("../..") -[node name="PlayerInteraction" type="Node" parent="Scripts" node_paths=PackedStringArray("interactableArea")] +[node name="PlayerInteraction" type="Node" parent="Scripts" node_paths=PackedStringArray("interactableArea", "player")] script = ExtResource("3_24gqh") interactableArea = NodePath("../../InteractableArea") +player = NodePath("../..") [node name="CollisionShape3D" type="CollisionShape3D" parent="."] shape = SubResource("CapsuleShape3D_2m2ha") diff --git a/scenes/singletons/Transition.tscn b/scenes/singletons/Transition.tscn new file mode 100644 index 0000000..56a5f82 --- /dev/null +++ b/scenes/singletons/Transition.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=2 format=3 uid="uid://i4ukelrrsujw"] + +[ext_resource type="Script" uid="uid://iu3m73wtjlho" path="res://scripts/singletons/Transition.gd" id="1_isjic"] + +[node name="Transition" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_isjic") + +[node name="Overlay" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 diff --git a/scripts/InteractableArea.gd b/scripts/InteractableArea.gd index 752233c..a80f802 100644 --- a/scripts/InteractableArea.gd +++ b/scripts/InteractableArea.gd @@ -1,3 +1,3 @@ class_name InteractableArea extends Area3D -signal interactEvent \ No newline at end of file +signal interactEvent(playerEntity:Player) \ No newline at end of file diff --git a/scripts/entities/NPC.gd b/scripts/entities/NPC.gd index f2415ba..bfa4041 100644 --- a/scripts/entities/NPC.gd +++ b/scripts/entities/NPC.gd @@ -8,46 +8,146 @@ enum InteractType { # 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) + + # 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) + TRANSITION.fadeOutEnd.connect(onFadeOutEnd) + return + + # Reset Camera? + if previousCamera: + previousCamera.current = true + previousCamera = null func _enter_tree() -> void: - $InteractableArea.interactEvent.connect(_on_interact) + $InteractableArea.interactEvent.connect(onInteract) func _exit_tree() -> void: - $InteractableArea.interactEvent.disconnect(_on_interact) + $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 _on_interact() -> void: +func onInteract(playerEntity:Player) -> void: + # Reset state + previousCamera = null nextTextIndex = 0 + playerEnt = playerEntity + conversationEnding = false + match interactType: InteractType.TEXTBOX: - if interactTexts.size() == 0: + PAUSE.cutscenePause() + + # If a camera is set, switch to it, otherwise chat immediately. + if !interactCamera: + showTexts() return - UI.TEXTBOX.setText(interactTexts[nextTextIndex]) - UI.TEXTBOX.textboxClosing.connect(onTextboxClosing) - return + + # Fade out. + TRANSITION.fade(TRANSITION.FadeType.FADE_OUT) + TRANSITION.fadeOutEnd.connect(onFadeOutEnd) InteractType.CUTSCENE: - if cutscene: - cutscene.start() - return + if !cutscene: + return + cutscene.start() + _: return - func onTextboxClosing() -> void: nextTextIndex += 1 + + # More text? if nextTextIndex < interactTexts.size(): UI.TEXTBOX.setText(interactTexts[nextTextIndex]) else: - UI.TEXTBOX.textboxClosing.disconnect(onTextboxClosing) + endTexts() + +func onFadeOutEnd() -> void: + # Begin fade back in. + TRANSITION.fadeOutEnd.disconnect(onFadeOutEnd) + TRANSITION.fade(TRANSITION.FadeType.FADE_IN) + 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() diff --git a/scripts/entities/Player.gd b/scripts/entities/Player.gd new file mode 100644 index 0000000..e69de29 diff --git a/scripts/entities/player/PlayerInteraction.gd b/scripts/entities/player/PlayerInteraction.gd index 164cacf..4572626 100644 --- a/scripts/entities/player/PlayerInteraction.gd +++ b/scripts/entities/player/PlayerInteraction.gd @@ -1,6 +1,7 @@ class_name PlayerInteraction extends Node @export var interactableArea:Area3D +@export var player:CharacterBody3D func canInteract() -> bool: if PAUSE.isMovementPaused(): @@ -26,4 +27,4 @@ func _process(delta: float) -> void: if !interactable: return - interactable.interactEvent.emit() + interactable.interactEvent.emit(player) diff --git a/scripts/entities/player/PlayerMovement.gd b/scripts/entities/player/PlayerMovement.gd index 969d8f4..3346367 100644 --- a/scripts/entities/player/PlayerMovement.gd +++ b/scripts/entities/player/PlayerMovement.gd @@ -5,9 +5,6 @@ const SPEED = 8.0 @export var player:CharacterBody3D -func _ready() -> void: - player = get_parent().get_parent() - func canMove() -> bool: if PAUSE.isMovementPaused(): return false diff --git a/scripts/singletons/Pause.gd b/scripts/singletons/Pause.gd index 4e5a072..47f2c82 100644 --- a/scripts/singletons/Pause.gd +++ b/scripts/singletons/Pause.gd @@ -1,6 +1,16 @@ class_name PauseSingleton extends Node +var cutscenePaused:bool = false + +func cutscenePause() -> void: + cutscenePaused = true + +func cutsceneResume() -> void: + cutscenePaused = false + func isMovementPaused() -> bool: + if cutscenePaused: + return true if !UI.TEXTBOX.isClosed: return true return false \ No newline at end of file diff --git a/scripts/singletons/Transition.gd b/scripts/singletons/Transition.gd new file mode 100644 index 0000000..2fbff1e --- /dev/null +++ b/scripts/singletons/Transition.gd @@ -0,0 +1,79 @@ +class_name TransitionSingleton extends Control + +enum FadeType { + NONE, + FADE_IN, + FADE_OUT +} + +signal fadeOutStart +signal fadeOutEnd +signal fadeInStart +signal fadeInEnd +signal fadeUpdate(t:float) + +var fadeType:FadeType = FadeType.NONE +var fadeDuration:float = 0.4 +var fadeTime:float = 0.0 + +func _enter_tree() -> void: + $Overlay.visible = false + +func _process(delta: float) -> void: + if fadeType == FadeType.NONE: + return + + fadeTime += delta + var t:float = fadeTime / fadeDuration + + # Get destination alpha type. + var destAlpha:float = 0.0 + var srcAlpha:float + if fadeType == FadeType.FADE_IN: + srcAlpha = 1.0 + destAlpha = 0.0 + elif fadeType == FadeType.FADE_OUT: + srcAlpha = 0.0 + destAlpha = 1.0 + + # End? + if t >= 1.0: + fadeUpdate.emit(1.0) + var cFade = fadeType + print("Transition: Fade complete") + fadeType = FadeType.NONE + + if cFade == FadeType.FADE_OUT: + $Overlay.visible = true + fadeOutEnd.emit() + elif cFade == FadeType.FADE_IN: + $Overlay.visible = false + fadeInEnd.emit() + + $Overlay.color.a = destAlpha + return + + # TODO: Use curves + $Overlay.color.a = srcAlpha + (destAlpha - srcAlpha) * t + fadeUpdate.emit(t) + pass + +func fade( + fade:FadeType = FadeType.FADE_IN, + duration:float = 0.4, + color:Color = Color(0, 0, 0, 1), +): + $Overlay.visible = false + $Overlay.color = color + fadeDuration = duration + fadeTime = 0 + fadeType = fade + + if fade == FadeType.FADE_IN: + fadeInStart.emit() + $Overlay.color.a = 0 + elif fade == FadeType.FADE_OUT: + fadeOutStart.emit() + $Overlay.color.a = 1 + + $Overlay.visible = true \ No newline at end of file diff --git a/scripts/singletons/Transition.gd.uid b/scripts/singletons/Transition.gd.uid new file mode 100644 index 0000000..419b9c3 --- /dev/null +++ b/scripts/singletons/Transition.gd.uid @@ -0,0 +1 @@ +uid://iu3m73wtjlho