Npc/BaseNpc.cs
using System;
using Clover.Components;
using Clover.Data;
using Clover.Interactable;
using Clover.Player;
using Sandbox.Diagnostics;
using Sandbox.States;
namespace Clover.Npc;
[Title( "Base Npc" )]
[Icon( "face" )]
[Category( "Clover/Npc" )]
public class BaseNpc : Component, IInteract
{
public enum NpcState
{
Idle,
Walking,
Interacting
}
[RequireComponent] public NavMeshAgent Agent { get; set; }
[Property] public string Name { get; set; }
public GameObject InteractionTarget;
public NpcState State { get; set; } = NpcState.Idle;
private TimeUntil _nextAction;
private TimeSince _startWalking;
private const float WalkRadius = 256f;
[Property] public DialogueCollection DialogueCollection { get; set; }
public void SetState( NpcState state )
{
Log.Info( $"BaseNpc SetState: {state}" );
State = state;
}
protected override void OnStart()
{
_nextAction = Random.Shared.Float( 3, 15 );
SetState( NpcState.Idle );
}
private void Idle()
{
Agent.Stop();
TargetPosition = null;
if ( _nextAction )
{
WalkToRandomTarget();
SetState( NpcState.Walking );
}
}
private void Walking()
{
LookAt( WorldPosition + Agent.Velocity.Normal );
if ( IsCloseToTarget )
{
_nextAction = Random.Shared.Float( 3, 15 );
SetState( NpcState.Idle );
}
else if ( _startWalking > 20f )
{
Log.Warning( "Villager Walking: Took too long to reach target" );
_nextAction = Random.Shared.Float( 3, 15 );
SetState( NpcState.Idle );
}
}
private void Interacting()
{
if ( InteractionTarget.IsValid() )
{
LookAt( InteractionTarget );
if ( InteractionTarget.WorldPosition.Distance( WorldPosition ) > 128f )
{
if ( InteractionTarget.Components.TryGet<PlayerCharacter>( out var player ) )
{
player.PlayerInteract.InteractionTarget = null;
}
_nextAction = Random.Shared.Float( 3, 8 );
EndInteraction();
}
}
else
{
_nextAction = Random.Shared.Float( 3, 8 );
EndInteraction();
}
}
public void LoadDialogue( Dialogue dialogue )
{
var window = DialogueManager.Instance.DialogueWindow;
if ( window == null )
{
Log.Error( "BaseNpc: No dialogue window found" );
return;
}
window.Enabled = true;
window.LoadDialogue( dialogue );
}
public Vector3? TargetPosition;
public bool IsCloseToTarget => TargetPosition.HasValue && WorldPosition.Distance( TargetPosition.Value ) < 32f;
public void WalkToRandomTarget()
{
var pos = Scene.NavMesh.GetClosestPoint( WorldPosition +
new Vector3( Random.Shared.Float( -WalkRadius, WalkRadius ),
Random.Shared.Float( -WalkRadius, WalkRadius ), 0 ) );
if ( !pos.HasValue )
{
Log.Error( "Villager WalkToRandomTarget: No valid position found" );
return;
}
var path = Scene.NavMesh.GetSimplePath( WorldPosition, pos.Value );
if ( path.Count == 0 )
{
Log.Error( "Villager WalkToRandomTarget: No path found" );
return;
}
if ( path.Count < 2 )
{
Log.Error( "Villager WalkToRandomTarget: Path is too short" );
return;
}
_startWalking = 0;
TargetPosition = pos.Value;
Log.Info( "Villager WalkToRandomTarget: " + TargetPosition );
Agent.MoveTo( TargetPosition.Value );
}
public virtual void StartInteract( PlayerCharacter player )
{
Log.Info( "BaseNpc StartInteract" );
if ( InteractionTarget.IsValid() )
{
Log.Error( "BaseNpc StartInteract: Busy" );
return;
}
player.PlayerInteract.InteractionTarget = GameObject;
player.ModelLookAt( WorldPosition );
InteractionTarget = player.GameObject;
SetState( NpcState.Interacting );
DispatchDialogue();
}
public void EndInteraction()
{
if ( InteractionTarget.IsValid() )
{
if ( InteractionTarget.Components.TryGet<PlayerCharacter>( out var player ) )
{
player.PlayerInteract.InteractionTarget = null;
}
}
InteractionTarget = null;
SetState( NpcState.Idle );
}
protected void DispatchDialogue()
{
if ( DialogueCollection == null )
{
Log.Error( "BaseNpc DispatchDialogue: No dialogue collection found" );
EndInteraction();
return;
}
Assert.NotNull( DialogueManager.Instance.DialogueWindow, "BaseNpc DispatchDialogue: No dialogue window found" );
Assert.NotNull( PlayerCharacter.Local, "BaseNpc DispatchDialogue: No player found" );
Assert.NotNull( DialogueCollection.Entries, "BaseNpc DispatchDialogue: No dialogue entries found" );
Assert.True( DialogueCollection.Entries.Count > 0, "BaseNpc DispatchDialogue: No dialogue entries found" );
var entries = DialogueCollection.Entries.ToList().OrderBy( x => Guid.NewGuid() );
foreach ( var dialogue in entries )
{
if ( dialogue.Condition == null || dialogue.Condition.Invoke( DialogueManager.Instance.DialogueWindow,
PlayerCharacter.Local,
new List<GameObject>() { GameObject } ) )
{
DialogueManager.Instance.DialogueWindow.SetData( "PlayerName", PlayerCharacter.Local.PlayerName );
DialogueManager.Instance.DialogueWindow.SetTarget( 0, GameObject );
LoadDialogue( dialogue.DialogueData );
DialogueManager.Instance.DialogueWindow.OnDialogueEnd += EndInteraction;
return;
}
}
}
public string GetInteractName()
{
return $"Talk to {Name}";
}
protected Rotation TargetLookAt;
public void LookAt( Vector3 target )
{
var dir = (target - WorldPosition).Normal;
var rot = Rotation.LookAt( dir );
TargetLookAt = rot.Angles().WithPitch( 0 );
}
public void LookAt( GameObject target )
{
LookAt( target.WorldPosition );
}
protected override void OnFixedUpdate()
{
StateLogic();
WorldRotation = Rotation.Slerp( WorldRotation, TargetLookAt, Time.Delta * 5f );
}
protected virtual void StateLogic()
{
switch ( State )
{
case NpcState.Idle:
Idle();
break;
case NpcState.Walking:
Walking();
break;
case NpcState.Interacting:
Interacting();
break;
}
}
}