using System; using System.ComponentModel; using System.Threading; using KitsuneCafe.Extension; using KitsuneCafe.Sys; using R3; using Unity.AppUI.MVVM; using Unity.Properties; using UnityEngine; namespace KitsuneCafe.Entities { public enum LandingForce : byte { None, Soft, Hard } [ObservableObject] public partial class AirMotor : MonoBehaviour, INotifyPropertyChanged { [Header("Dependencies")] [SerializeField] private new Rigidbody rigidbody; [SerializeField] private Spring spring; [SerializeField] private Motor motor; [Space] [SerializeField] private Duration landingPredictionTime = TimeSpan.FromSeconds(0.1); [SerializeField] private Duration landingRecoveryTime = TimeSpan.FromSeconds(1); [SerializeField] private Duration hardLandingRecoveryTime = TimeSpan.FromSeconds(2); [SerializeField] private float hardLandingThreshold = Physics.gravity.y; [SerializeField] private LayerMask layerMask; [ObservableProperty] [AlsoNotifyChangeFor(nameof(IsFalling))] private bool isGrounded = false; [CreateProperty(ReadOnly = true)] public bool IsFalling => !IsGrounded && !IsLanding; [ObservableProperty] [AlsoNotifyChangeFor(nameof(IsFalling))] private bool isLanding = false; public event EventHandler Landed = delegate { }; private CancellationTokenSource disableCancellationSource; private void OnValidate() { this.TryGetComponentIfNull(ref rigidbody); this.TryGetComponentIfNull(ref spring); this.TryGetComponentIfNull(ref motor); } private void OnEnable() { disableCancellationSource = new(); var d = Disposable.CreateBuilder(); var groundedSource = spring.ObservePropertyChanged(x => x.IsColliding); var willLandSource = groundedSource.SwitchIfElse( Observable.Empty<(bool, LandingForce)>(), Observable.EveryUpdate(UnityFrameProvider.FixedUpdate) .Select(_ => WillLandWithin(landingPredictionTime, Time.fixedDeltaTime)) ) .DistinctUntilChanged(); var landingSource = ObserveLanding(willLandSource); willLandSource.Where(v => v.Item1).Subscribe(v => OnLanding(v.Item2)).AddTo(ref d); groundedSource.Subscribe(grounded => IsGrounded = grounded).AddTo(ref d); landingSource.Subscribe(landing => { motor.CanMove = !landing; IsLanding = landing; }).AddTo(ref d); d.RegisterTo(disableCancellationSource.Token); } private void OnDisable() { disableCancellationSource.Cancel(); } private (bool, LandingForce) WillLandWithin(TimeSpan timeSpan, float deltaTime) { var currentPosition = rigidbody.position; var velocity = rigidbody.linearVelocity; for (float i = 0; i < timeSpan.TotalSeconds; i += deltaTime) { velocity += Physics.gravity * deltaTime; var nextPosition = currentPosition + velocity * deltaTime; if (Raycast(currentPosition, nextPosition - currentPosition, out var hit)) { return (true, GetLandingForce(velocity)); } currentPosition = nextPosition; } return (false, LandingForce.None); } private bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hit) { return Physics.Raycast(origin, direction.normalized, out hit, direction.magnitude, layerMask); } private void OnLanding(LandingForce force) { Landed?.Invoke(this, force); } private Observable ObserveLanding(Observable<(bool, LandingForce)> landingSource) { return Observable.Merge( landingSource.Select(v => v.Item1).WhereTrue(), landingSource.Select(v => GetLandingRecoveryTime(v.Item2)) .SelectSwitch(duration => Observable.Timer(duration, UnityTimeProvider.FixedUpdate)) .Select(_ => false) ); } private LandingForce GetLandingForce(Vector3 velocity) { return (velocity.y <= hardLandingThreshold) switch { true => LandingForce.Hard, _ => LandingForce.Soft }; } private Duration GetLandingRecoveryTime(LandingForce force) { return force switch { LandingForce.Soft => landingRecoveryTime, LandingForce.Hard => hardLandingRecoveryTime, _ => TimeSpan.Zero }; } private Observable ObserveLandingForce(Observable groundedSource) { var landingForceSource = Observable.Merge( Observable.Return(LandingForce.Soft), Observable.EveryUpdate(UnityFrameProvider.FixedUpdate) .SkipWhile(_ => rigidbody.linearVelocity.y > hardLandingThreshold) .Select(_ => LandingForce.Hard) .Take(1) ); return groundedSource.WhereFalse() .SelectSwitch(_ => Observable.ZipLatest( groundedSource.WhereTrue(), landingForceSource, (_, landing) => landing ) ); } } }