< Summary

Information
Class: Trailblazer.Navigation.Motor.PlatformLocomotion
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Motor/Locomotion/PlatformLocomotion.cs
Line coverage
100%
Covered lines: 146
Uncovered lines: 0
Coverable lines: 146
Total lines: 431
Line coverage: 100%
Branch coverage
100%
Covered branches: 106
Total branches: 106
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor()100%11100%
get_IsEnabled()100%11100%
set_IsEnabled(...)100%22100%
RequireContext()100%22100%
get_InvDeltaTime()100%11100%
get_IsActive()100%44100%
get_IsLockedToPlatform()100%11100%
get_IsHoldingPlatform()100%44100%
get_InertiaApplied()100%44100%
UpdatePlatformVelocity()100%1010100%
BindContext(...)100%11100%
GetPlatformInfluence(...)100%1010100%
SetHoldPlatform(...)100%11100%
TickHoldOnPlatform()100%88100%
HandlePlatformChange(...)100%66100%
DidPlatformChange(...)100%44100%
NormalizeKinematicPlatform(...)100%44100%
ResolveMovementTransfer(...)100%44100%
ClearHoldPlatformIfInactive(...)100%44100%
TryRefreshExistingPlatform(...)100%88100%
SwapActivePlatform(...)100%66100%
HandlePlatformMovement(...)100%11100%
GetAttachmentTransform()100%44100%
RecordData(...)100%2222100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Navigation/Motor/Locomotion/PlatformLocomotion.cs

#LineLine coverage
 1//=======================================================================
 2// PlatformLocomotion.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2024-present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using System;
 9using System.Runtime.CompilerServices;
 10using Chronicler;
 11using FixedMathSharp;
 12using Trailblazer.Support;
 13
 14namespace Trailblazer.Navigation.Motor;
 15
 16/// <summary>
 17/// Handles movement adjustments when the scout is standing on a moving platform or surface.
 18/// </summary>
 19/// <remarks>
 20/// This locomotion system tracks platform velocity, rotation, and movement transfer behavior.
 21/// It allows the scout to inherit motion from platforms and supports different transfer states.
 22/// </remarks>
 23public class PlatformLocomotion : ILocomotion
 24{
 25    private TrailblazerWorldContext? _context;
 26
 27    #region Constants
 28
 29    /// <summary>
 30    /// Default height adjustment applied when standing on a moving platform.
 31    /// </summary>
 132    public static readonly Fixed64 DefaultHeightAdjust = Fixed64.FromRaw(0x80000000L); // 0.5f;
 33
 34    /// <summary>
 35    /// Maximum number of frames the scout can remain attached to a platform before release.
 36    /// </summary>
 37    public const int MaxHoldPlatformFrames = 2;
 38
 39    #endregion
 40
 41    #region Configuration State
 42
 43    /// <summary>
 44    /// Determines whether platform locomotion is enabled.
 45    /// </summary>
 121146    private bool _isEnabled = true;
 47
 48    /// <summary>
 49    /// The height offset applied when interacting with moving platforms.
 50    /// </summary>
 121151    public Fixed64 HeightAdjust = DefaultHeightAdjust;
 52
 53    #endregion
 54
 55    #region Transient State
 56
 57    /// <inheritdoc cref="ILocomotion.IsEnabled"/>
 58    public bool IsEnabled
 59    {
 1521160        get => _isEnabled;
 61        set
 62        {
 763            _isEnabled = value;
 764            if (!_isEnabled)
 65            {
 566                _preservePreviousTransformForAttachment = false;
 567                this.ClearTransientState();
 68            }
 769        }
 70    }
 71
 72    /// <summary>
 73    /// Indicates whether the scout has just landed on a new platform.
 74    /// </summary>
 75    [Transient]
 76    public bool IsNewPlatform { get; set; }
 77
 78    /// <summary>
 79    /// The currently active platform the scout is standing on, if any.
 80    /// </summary>
 81    [Transient]
 82    public PlatformSnapshot? ActivePlatform { get; set; }
 83
 84    /// <summary>
 85    /// The previously active platform, used for calculating platform velocity and movement transfer.
 86    /// This is cleared when the scout is not on a platform or when platform locomotion is disabled.
 87    ///
 88    /// </summary>
 89    [Transient]
 90    public PlatformSnapshot? PreviousPlatform { get; set; }
 91
 92    /// <summary>
 93    /// A flag to preserve the previous platform's transform for attachment calculations,
 94    /// used when refreshing the same platform with a new transform.
 95    /// </summary>
 96    private bool _preservePreviousTransformForAttachment;
 97
 98    /// <summary>
 99    /// The platform snapshot that the scout is currently holding onto, if any.
 100    /// </summary>
 101    [Transient]
 102    public PlatformSnapshot? HoldPlatform { get; set; }
 103
 104    /// <summary>
 105    /// Defines how movement is transferred from the platform to the scout.
 106    /// </summary>
 107    [Transient]
 108    public MotionTransfer MovementTransfer { get; set; }
 109
 110    /// <summary>
 111    /// The local position of the scout relative to the platform.
 112    /// </summary>
 113    [Transient]
 114    public Vector3d ScoutLocalPoint { get; set; }
 115
 116    /// <summary>
 117    /// The local rotation of the scout relative to the platform.
 118    /// </summary>
 119    [Transient(typeof(FixedQuaternion), nameof(FixedQuaternion.Identity))]
 120    public FixedQuaternion ScoutLocalRotation { get; set; } = FixedQuaternion.Identity;
 121
 122    /// <summary>
 123    /// The velocity of the platform.
 124    /// </summary>
 125    [Transient]
 126    public Vector3d PlatformVelocity { get; set; }
 127
 128    /// <summary>
 129    /// The last known platform velocity when the scout is airborne.
 130    /// </summary>
 131    [Transient]
 132    public Vector3d FramePlatformVelocity { get; set; }
 133
 134    private TrailblazerWorldContext RequireContext() =>
 357135        _context ?? throw new InvalidOperationException("PlatformLocomotion requires an explicit TrailblazerWorldContext
 136
 357137    private Fixed64 InvDeltaTime => RequireContext().InvDeltaTime;
 138
 139    /// <summary>
 140    /// The number of frames the scout has been holding onto a platform.
 141    /// </summary>
 142    [Transient]
 143    public int HoldPlatformFrames { get; private set; }
 144
 145    /// <summary>
 146    /// Gets a value indicating whether the component is currently active and supports kinematic motion.
 147    /// </summary>
 4344148    public bool IsActive => IsEnabled && ActivePlatform?.SupportsKinematicMotion == true;
 149
 150    /// <summary>
 151    /// Gets a value indicating whether the object is permanently locked to the platform.
 152    /// </summary>
 446153    public bool IsLockedToPlatform => MovementTransfer == MotionTransfer.PermaLocked;
 154
 155    /// <summary>
 156    /// Gets a value indicating whether the current object is holding a platform that supports kinematic motion.
 157    /// </summary>
 1984158    public bool IsHoldingPlatform => IsEnabled && HoldPlatform?.SupportsKinematicMotion == true;
 159
 160    /// <summary>
 161    /// Indicates whether platform inertia (initial velocity transfer) has been applied.
 162    /// </summary>
 1969163    public bool InertiaApplied => IsEnabled
 1969164        && (MovementTransfer == MotionTransfer.InitTransfer || MovementTransfer == MotionTransfer.PermaTransfer);
 165
 166    #endregion
 167
 168    #region Methods
 169
 170    /// <summary>
 171    /// Updates the platform velocity based on movement from the last frame.
 172    /// </summary>
 173    public void UpdatePlatformVelocity()
 174    {
 2191175        if (!IsEnabled) return;
 176
 2187177        if (ActivePlatform?.SupportsKinematicMotion != true)
 178        {
 1741179            PlatformVelocity = Vector3d.Zero;
 1741180            _preservePreviousTransformForAttachment = false;
 1741181            return;
 182        }
 183
 446184        if (!IsNewPlatform)
 185        {
 357186            Vector3d currentPoint = ActivePlatform.Value.Transform.TransformPoint(ScoutLocalPoint);
 357187            Vector3d previousPoint = PreviousPlatform?.Transform.TransformPoint(ScoutLocalPoint) ?? Vector3d.Zero;
 188
 189            // Store platform velocity to use as a canceling force
 357190            PlatformVelocity = (currentPoint - previousPoint) * InvDeltaTime;
 191        }
 192
 445193        PreviousPlatform = ActivePlatform;
 445194        IsNewPlatform = false;
 445195        _preservePreviousTransformForAttachment = false;
 445196    }
 197
 198    internal void BindContext(TrailblazerWorldContext context)
 199    {
 1282200        TrailblazerWorldContext.ThrowIfUnusable(context);
 1282201        _context = context;
 1282202    }
 203
 204    /// <summary>
 205    /// Applies movement adjustments due to platform motion, ensuring the object inherits platform movement correctly.
 206    /// </summary>
 207    /// <remarks>
 208    /// This method updates the object’s position and rotation based on the platform’s transform,
 209    /// preventing unwanted movement shifts when transitioning between platforms.
 210    /// </remarks>
 211    public void GetPlatformInfluence(
 212        Vector3d position,
 213        FixedQuaternion rotation,
 214        out Vector3d positionDelta,
 215        out FixedQuaternion rotationDelta)
 216    {
 217        // Apply platform rotation first THEN apply platform movement
 206218        FixedQuaternion targetRotation = ActivePlatform?.Transform.Rotation * ScoutLocalRotation ?? FixedQuaternion.Iden
 206219        if (targetRotation != FixedQuaternion.Identity)
 220        {
 84221            FixedQuaternion rotDelta = targetRotation * rotation.Inverse();
 84222            rotationDelta = rotDelta;
 223        }
 224        else
 122225            rotationDelta = FixedQuaternion.Identity;
 226
 206227        Vector3d newGlobalPoint = ActivePlatform?.Transform.TransformPoint(ScoutLocalPoint) ?? Vector3d.Zero;
 206228        position.Y += HeightAdjust;
 206229        positionDelta = newGlobalPoint - position;
 206230    }
 231
 232    /// <summary>
 233    /// Assigns the scout to a sampled platform state, initiating a hold state.
 234    /// </summary>
 235    /// <param name="platform">The sampled platform snapshot to attach to.</param>
 236    public void SetHoldPlatform(PlatformSnapshot? platform)
 237    {
 13238        HoldPlatform = NormalizeKinematicPlatform(platform);
 13239        HoldPlatformFrames = 0;
 13240    }
 241
 242    /// <summary>
 243    /// Updates the platform hold state, releasing the hold if the hold duration expires.
 244    /// </summary>
 245    /// <returns>True if the scout should detach from the platform; otherwise, false.</returns>
 246    public bool TickHoldOnPlatform()
 247    {
 15248        if (!IsHoldingPlatform)
 1249            return false;
 250
 14251        HoldPlatformFrames++;
 14252        if (HoldPlatformFrames >= MaxHoldPlatformFrames)
 253        {
 5254            HoldPlatformFrames = 0;
 5255            if (!ActivePlatform.HasValue || HoldPlatform!.Value != ActivePlatform.Value)
 4256                return true;
 257        }
 258
 10259        return false;
 260    }
 261
 262    /// <summary>
 263    /// Handles changes in the platform state based on the specified ground condition. Updates platform-related movement
 264    /// </summary>
 265    /// <remarks>
 266    /// This method should be called whenever the underlying platform or ground condition changes,
 267    /// such as when stepping onto a new platform or leaving one.
 268    /// It resets and updates platform movement and transfer state as needed.
 269    /// </remarks>
 270    /// <param name="condition">The ground condition representing the current platform state. May be null if there is no
 271    public void HandlePlatformChange(GroundCondition? condition)
 272    {
 273        // If we hit a new platform, reset platform state
 2559274        if (!IsEnabled)
 2275            return;
 276
 277        // Clear it to avoid double-applying next frame
 2557278        FramePlatformVelocity = Vector3d.Zero;
 2557279        PlatformSnapshot? refreshedPlatform = NormalizeKinematicPlatform(condition?.Platform);
 2557280        MovementTransfer = ResolveMovementTransfer(refreshedPlatform, condition);
 2557281        ClearHoldPlatformIfInactive(refreshedPlatform);
 282
 2557283        if (TryRefreshExistingPlatform(refreshedPlatform))
 2411284            return;
 285
 146286        SwapActivePlatform(refreshedPlatform);
 146287    }
 288
 289    /// <summary>
 290    /// Determines if the object has transitioned onto a different platform.
 291    /// </summary>
 292    /// <param name="newPlatform"></param>
 293    /// <returns>True if the object is on a new platform; otherwise, false.</returns>
 2557294    private bool DidPlatformChange(PlatformSnapshot? newPlatform) => ActivePlatform != newPlatform;
 295
 296    private static PlatformSnapshot? NormalizeKinematicPlatform(PlatformSnapshot? platform)
 297    {
 2801298        return platform?.SupportsKinematicMotion == true
 2801299            ? platform
 2801300            : null;
 301    }
 302
 303    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 304    private static MotionTransfer ResolveMovementTransfer(PlatformSnapshot? refreshedPlatform, GroundCondition? conditio
 305    {
 2557306        return refreshedPlatform?.SupportsKinematicMotion == true
 2557307            // refreshedPlatform comes from condition.Platform, so a kinematic snapshot implies condition exists.
 2557308            ? condition!.Value.MotionTransferState
 2557309            : MotionTransfer.None;
 310    }
 311
 312    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 313    private void ClearHoldPlatformIfInactive(PlatformSnapshot? refreshedPlatform)
 314    {
 2557315        if (refreshedPlatform?.SupportsKinematicMotion == true)
 564316            return;
 317
 1993318        HoldPlatform = null;
 1993319        HoldPlatformFrames = 0;
 1993320    }
 321
 322    private bool TryRefreshExistingPlatform(PlatformSnapshot? refreshedPlatform)
 323    {
 2557324        if (DidPlatformChange(refreshedPlatform))
 146325            return false;
 326
 2411327        bool hasTransformRefresh = ActivePlatform?.SupportsKinematicMotion == true
 2411328            && !ActivePlatform.Value.Transform.Equals(refreshedPlatform!.Value.Transform);
 329
 2411330        if (hasTransformRefresh)
 9331            PreviousPlatform = ActivePlatform;
 332
 333        // Same platform id, newer transform: refresh the snapshot without marking a platform swap.
 2411334        ActivePlatform = refreshedPlatform;
 2411335        _preservePreviousTransformForAttachment = hasTransformRefresh;
 2411336        return true;
 337    }
 338
 339    private void SwapActivePlatform(PlatformSnapshot? refreshedPlatform)
 340    {
 146341        PreviousPlatform = ActivePlatform?.SupportsKinematicMotion != true
 146342            ? refreshedPlatform
 146343            : ActivePlatform;
 146344        ActivePlatform = refreshedPlatform;
 345
 146346        IsNewPlatform = refreshedPlatform?.SupportsKinematicMotion == true;
 146347        _preservePreviousTransformForAttachment = false;
 146348    }
 349
 350    /// <summary>
 351    /// Updates platform movement by synchronizing the object's position and rotation with the platform it is standing o
 352    /// </summary>
 353    /// <remarks>
 354    /// This method prevents unwanted movement shifts when transitioning between platforms, ensuring smooth locomotion.
 355    /// </remarks>
 356    public void HandlePlatformMovement(Vector3d position, FixedQuaternion rotation)
 357    {
 194358        position.Y += HeightAdjust;
 194359        Fixed4x4 attachmentTransform = GetAttachmentTransform();
 194360        ScoutLocalPoint = Fixed4x4.InverseTransformPoint(
 194361            attachmentTransform,
 194362            position);
 363
 194364        ScoutLocalRotation = attachmentTransform.Rotation.Inverse() * rotation;
 194365    }
 366
 367    private Fixed4x4 GetAttachmentTransform()
 368    {
 194369        if (_preservePreviousTransformForAttachment)
 8370            return PreviousPlatform!.Value.Transform;
 371
 186372        return ActivePlatform?.Transform ?? Fixed4x4.Identity;
 373    }
 374
 375    #endregion
 376
 377    /// <inheritdoc />
 378    public void RecordData(IChronicler chronicler)
 379    {
 164380        RecordValues.Look(chronicler, ref _isEnabled, "IsEnabled", true);
 164381        RecordValues.Look(chronicler, ref HeightAdjust, "HeightAdjust", DefaultHeightAdjust);
 382
 164383        bool isNewPlatform = IsNewPlatform;
 164384        PlatformSnapshot? activePlatform = ActivePlatform;
 164385        PlatformSnapshot? previousPlatform = PreviousPlatform;
 164386        PlatformSnapshot? holdPlatform = HoldPlatform;
 164387        MotionTransfer movementTransfer = MovementTransfer;
 164388        Vector3d scoutLocalPoint = ScoutLocalPoint;
 164389        FixedQuaternion scoutLocalRotation = ScoutLocalRotation;
 164390        Vector3d platformVelocity = PlatformVelocity;
 164391        Vector3d framePlatformVelocity = FramePlatformVelocity;
 164392        int holdPlatformFrames = HoldPlatformFrames;
 393
 164394        RecordValues.Look(chronicler, ref isNewPlatform, "IsNewPlatform", false);
 164395        RecordValues.Look(chronicler, ref activePlatform, "ActivePlatform", null);
 164396        RecordValues.Look(chronicler, ref previousPlatform, "PreviousPlatform", null);
 164397        RecordValues.Look(chronicler, ref holdPlatform, "HoldPlatform", null);
 164398        RecordValues.Look(chronicler, ref movementTransfer, "MovementTransfer", MotionTransfer.None);
 164399        RecordValues.Look(chronicler, ref scoutLocalPoint, "ScoutLocalPoint", Vector3d.Zero);
 164400        RecordValues.Look(chronicler, ref scoutLocalRotation, "ScoutLocalRotation", FixedQuaternion.Identity);
 164401        RecordValues.Look(chronicler, ref platformVelocity, "PlatformVelocity", Vector3d.Zero);
 164402        RecordValues.Look(chronicler, ref framePlatformVelocity, "FramePlatformVelocity", Vector3d.Zero);
 164403        RecordValues.Look(chronicler, ref holdPlatformFrames, "HoldPlatformFrames", 0);
 404
 164405        if (chronicler.Mode == SerializationMode.Loading)
 406        {
 77407            ActivePlatform = NormalizeKinematicPlatform(activePlatform);
 77408            PreviousPlatform = NormalizeKinematicPlatform(previousPlatform);
 77409            HoldPlatform = NormalizeKinematicPlatform(holdPlatform);
 77410            IsNewPlatform = ActivePlatform?.SupportsKinematicMotion == true && isNewPlatform;
 77411            MovementTransfer = ActivePlatform?.SupportsKinematicMotion == true
 77412                ? movementTransfer
 77413                : MotionTransfer.None;
 77414            ScoutLocalPoint = scoutLocalPoint;
 77415            ScoutLocalRotation = scoutLocalRotation;
 77416            PlatformVelocity = ActivePlatform?.SupportsKinematicMotion == true
 77417                ? platformVelocity
 77418                : Vector3d.Zero;
 77419            FramePlatformVelocity = ActivePlatform?.SupportsKinematicMotion == true
 77420                ? framePlatformVelocity
 77421                : Vector3d.Zero;
 77422            HoldPlatformFrames = HoldPlatform?.SupportsKinematicMotion == true
 77423                ? holdPlatformFrames
 77424                : 0;
 77425            _preservePreviousTransformForAttachment = false;
 426
 77427            if (!_isEnabled)
 2428                this.ClearTransientState();
 429        }
 164430    }
 431}