using System;
using System.Collections;
using System.Threading.Tasks;
using Convai.Scripts.Runtime.Core;
using Convai.Scripts.Runtime.Features.LipSync;
using Convai.Scripts.Runtime.LoggerSystem;
using Service;
using UnityEngine;
namespace Convai.Scripts.Runtime.Features
{
///
/// This class is responsible for handling out all the tasks related to NPC to NPC conversation for a NPC of a group
///
public class ConvaiGroupNPCController : MonoBehaviour
{
///
/// Used to set Player GameObject Transform and lip-sync
///
private void Awake()
{
if (playerTransform == null) playerTransform = Camera.main.transform;
ConvaiNPC = GetComponent();
CONVERSATION_DISTANCE_THRESHOLD = conversationDistanceThreshold == 0 ? Mathf.Infinity : conversationDistanceThreshold;
TryGetComponent(out _lipSync);
}
///
/// Starts coroutine for player vicinity check and subscribe to necessary events
///
private void Start()
{
_npcGroup = NPC2NPCConversationManager.Instance.npcGroups.Find(c => c.BelongToGroup(this));
otherNPC = _npcGroup.GroupNPC1 == this ? _npcGroup.GroupNPC2 : _npcGroup.GroupNPC1;
_checkPlayerVicinityCoroutine = StartCoroutine(CheckPlayerVicinity());
if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio)) convaiNPCAudio.OnAudioTranscriptAvailable += HandleAudioTranscriptAvailable;
}
///
/// Unsubscribes to the events and stops the coroutine
///
private void OnDestroy()
{
if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio)) convaiNPCAudio.OnAudioTranscriptAvailable -= HandleAudioTranscriptAvailable;
if(_npc2NPCGrpcClient!=null) _npc2NPCGrpcClient.OnTranscriptAvailable -= HandleTranscriptAvailable;
if (_checkPlayerVicinityCoroutine != null) StopCoroutine(_checkPlayerVicinityCoroutine);
}
///
/// Shows speech bubble and adds the received text to final transcript
///
///
private void HandleAudioTranscriptAvailable(string transcript)
{
if (IsInConversationWithAnotherNPC)
ShowSpeechBubble?.Invoke(transcript);
}
private void HandleTranscriptAvailable(string transcript, ConvaiGroupNPCController npcController)
{
if (npcController != this) return;
_finalResponseText += transcript;
}
///
/// Attaches the speech bubble to the NPC game-object
///
public void AttachSpeechBubble()
{
if (TryGetComponent(out ConvaiSpeechBubbleController _)) return;
gameObject.AddComponent().Initialize(speechBubblePrefab, this);
}
///
/// Destroys the speech bubble game-object
///
public void DetachSpeechBubble()
{
if (TryGetComponent(out ConvaiSpeechBubbleController convaiSpeechBubble)) Destroy(convaiSpeechBubble);
}
///
/// Store the references of the client and subscribe to the necessary events
///
///
public void InitializeNpc2NpcGrpcClient(NPC2NPCGRPCClient client)
{
_npc2NPCGrpcClient = client;
_npc2NPCGrpcClient.OnTranscriptAvailable += HandleTranscriptAvailable;
}
///
/// Every 0.5 seconds updates if player is near or not and fire events according to the state
///
///
private IEnumerator CheckPlayerVicinity()
{
bool previousState = false;
Vector3 previousPlayerPosition = Vector3.zero;
yield return new WaitForSeconds(0.1f);
while (true)
{
Vector3 currentPlayerPosition = playerTransform.transform.position;
// Check if the player has moved more than a certain threshold distance
if (Vector3.Distance(previousPlayerPosition, currentPlayerPosition) > PLAYER_MOVE_THRESHOLD)
{
// Calculate the distance between the NPC and the player
float distanceToPlayer = Vector3.Distance(transform.position, currentPlayerPosition);
// Check if the player is within the threshold distance
bool isPlayerCurrentlyNear = distanceToPlayer <= CONVERSATION_DISTANCE_THRESHOLD;
// If the player's current vicinity state is different from the previous state, raise the event
if (isPlayerCurrentlyNear != previousState && !ConvaiNPC.isCharacterActive)
{
OnPlayerVicinityChanged?.Invoke(isPlayerCurrentlyNear, this);
previousState = isPlayerCurrentlyNear; // Update the previous state
ConvaiLogger.Info($"Player is currently near {ConvaiNPC.characterName}: {isPlayerCurrentlyNear}", ConvaiLogger.LogCategory.Character);
}
previousPlayerPosition = currentPlayerPosition; // Update the player's previous position
// Check every half second
}
yield return new WaitForSeconds(0.5f);
}
}
///
/// Sends the text to the other NPC in the group
///
///
public async void SendTextDataNPC2NPC(string message)
{
if (_npc2NPCGrpcClient == null)
{
ConvaiLogger.Warn("No GRPC client initialized for this NPC.", ConvaiLogger.LogCategory.Character);
return;
}
try
{
CanRelayMessage = false;
await Task.Delay(500);
await _npc2NPCGrpcClient.SendTextData(
message,
ConvaiNPC.characterID,
ConvaiNPC.sessionID,
_lipSync != null,
FaceModel,
this);
}
catch (Exception ex)
{
ConvaiLogger.Warn($"Error sending message data for NPC2NPC: {ex.Message}", ConvaiLogger.LogCategory.Character);
}
}
public void EndOfResponseReceived()
{
if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio)) convaiNPCAudio.OnCharacterTalkingChanged += SendFinalTranscriptToOtherNPC;
ConversationManager.RelayMessage(_finalResponseText, this);
_finalResponseText = "";
}
private void SendFinalTranscriptToOtherNPC(bool isTalking)
{
if (IsInConversationWithAnotherNPC)
{
if (!isTalking)
{
ConversationManager.SwitchSpeaker(this);
if (TryGetComponent(out ConvaiNPCAudioManager convaiNPCAudio)) convaiNPCAudio.OnCharacterTalkingChanged -= SendFinalTranscriptToOtherNPC;
HideSpeechBubble?.Invoke();
}
else
{
ConvaiLogger.DebugLog($"{ConvaiNPC.characterName} is currently still talking. ", ConvaiLogger.LogCategory.Character);
}
}
}
public bool IsPlayerNearMe()
{
bool result = Vector3.Distance(transform.position, playerTransform.position) < CONVERSATION_DISTANCE_THRESHOLD;
ConvaiLogger.Info($"Player is near {CharacterName}: {result}", ConvaiLogger.LogCategory.Character);
return result;
}
public bool IsOtherNPCTalking()
{
return otherNPC.ConvaiNPC.IsCharacterTalking;
}
#region Serialized Fields
[Tooltip("The prefab for the speech bubble to display above the NPC. [Optional]")] [SerializeField]
private NPCSpeechBubble speechBubblePrefab;
[Tooltip("Attach the Main Player Transform here so that distance check can be performed")] [SerializeField]
private Transform playerTransform;
// The distance from the NPC to the player when the NPC will start talking
[Tooltip("The distance from the NPC to the player when the NPC will start talking. Set to 0 to disable this feature. [Optional]")] [SerializeField] [Range(0f, 100f)]
private float conversationDistanceThreshold = 5.0f;
#endregion
#region Private Attributes
private const float PLAYER_MOVE_THRESHOLD = 0.5f;
private float CONVERSATION_DISTANCE_THRESHOLD;
private string _finalResponseText = "";
private NPC2NPCGRPCClient _npc2NPCGrpcClient;
private ConvaiLipSync _lipSync;
private Coroutine _checkPlayerVicinityCoroutine;
private FaceModel FaceModel => _lipSync == null ? FaceModel.OvrModelName : _lipSync.faceModel;
#endregion
#region Events
public event Action ShowSpeechBubble;
public event Action HideSpeechBubble;
public event Action OnPlayerVicinityChanged;
private NPCGroup _npcGroup;
private ConvaiGroupNPCController otherNPC;
#endregion
#region Public Attributes
public bool CanRelayMessage { get; set; } = true;
public NPC2NPCConversationManager ConversationManager { get; set; }
public bool IsInConversationWithAnotherNPC { get; set; }
public string CharacterName => ConvaiNPC == null ? string.Empty : ConvaiNPC.characterName;
public string CharacterID => ConvaiNPC == null ? string.Empty : ConvaiNPC.characterID;
public ConvaiNPC ConvaiNPC { get; private set; }
#endregion
}
}