using System.Linq; using KitsuneCafe.Extension; using KitsuneCafe.Interaction; using ObservableCollections; using R3; using R3.Triggers; using UnityAtoms.BaseAtoms; using UnityEngine; namespace KitsuneCafe.Player { public class Interactor : MonoBehaviour, IInteractor { [SerializeField] private GameObject root; public GameObject Root => root; [SerializeField] private new Collider collider; [SerializeField] private GameObjectReference selectedObject; [SerializeField] private FloatEvent interactSource; [SerializeField, Tooltip("How far an object must move before being considered")] private float minimumChangeDelta = 0.1f; private float sqrtMovementDelta; public new T GetComponent() => Root.GetComponent(); public new bool TryGetComponent(out T component) => root.TryGetComponent(out component); private void Reset() { root = gameObject; collider = GetComponent(); } private void Awake() { selectedObject.Value = null; sqrtMovementDelta = Mathf.Sqrt(minimumChangeDelta); var d = Disposable.CreateBuilder(); var activeInteractables = TrackCollisions(collider, ref d); var selectedObjects = ObserveNearestInteractable( PositionChanged(transform, sqrtMovementDelta), activeInteractables ) .DefaultIfEmpty() .DistinctUntilChanged() .Do(selected => selectedObject.Value = selected?.gameObject) .SelectOrDefault(go => go.TryGetComponent(out IInteractable i) ? i : null) .Scan(null, SwapSelected); interactSource .Observe() .ToObservable() .WithLatestFrom(selectedObjects, (_, highlighted) => highlighted) .WhereNotNull() .Subscribe(interactable => { var result = interactable.Interact(this); if (!interactable.IsInteractable) { activeInteractables.Remove(interactable); } if (!result.IsOk) { Debug.Log($"interaction failed: {result}"); } }) .AddTo(ref d); d.RegisterTo(destroyCancellationToken); } private ObservableHashSet TrackCollisions(Collider collider, ref DisposableBuilder d) where T : IInteractable { var interactables = new ObservableHashSet(); collider.OnTriggerEnterAsObservable() .Subscribe(other => { if (other.TryGetComponent(out T interactable) && interactable.IsInteractable) { interactables.Add(interactable); } }) .AddTo(ref d); collider.OnTriggerExitAsObservable() .Subscribe(other => { if (other.TryGetComponent(out T interactable)) { interactables.Remove(interactable); } }) .AddTo(ref d); return interactables; } private Observable PositionChanged(Transform target, float delta, FrameProvider frameProvider = null) { frameProvider ??= UnityFrameProvider.FixedUpdate; return Observable.EveryValueChanged(target, t => t.position, frameProvider) .Scan((prev, cur) => cur.SqrDistance(prev) > delta ? cur : prev); } private Observable ObserveNearestInteractable(Observable positionSource, IObservableCollection interactables) => Observable.CombineLatest( interactables.ObserveChanged(), positionSource, (_, position) => Nearest(position, interactables) ); private IInteractable Nearest(Vector3 origin, IObservableCollection interactables) => interactables .Where(x => x.IsInteractable) .MinBy(x => origin.SqrDistance(x.gameObject.transform.position)); private IInteractable SwapSelected(IInteractable previous, IInteractable current) { previous?.Deselect(this); current?.Select(this); return current; } } }