< Summary

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

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
.ctor()100%11100%
get_Forces()100%11100%
get_Move()100%11100%
get_Platform()100%11100%
get_Jump()100%11100%
get_Fall()100%11100%
get_Slide()100%11100%
get_Water()100%11100%
get_Fly()100%11100%
get_Climb()100%11100%
Has(...)100%11100%
Has()100%11100%
TryGet(...)100%11100%
Require()100%22100%
Install(...)100%11100%
Replace(...)100%11100%
Remove()100%88100%
ApplyProfile(...)100%11100%
BindContext(...)100%11100%
ToProfile()100%11100%
ConfigureInstalledKinds(...)100%1010100%
RefreshInstalledKinds()100%1010100%
GetLocomotion(...)100%22100%
GetLocomotion(...)100%1414100%
GetLocomotions()100%1010100%
SetLocomotion(...)100%22100%
TryResolveLocomotionSlot(...)100%1616100%
SetLocomotion(...)100%88100%
BindInstalledLocomotions()100%11100%
BindLocomotion(...)100%1010100%
ClearReplacedLocomotion(...)100%44100%
SyncTransientState(...)100%88100%
ClearTransientState()100%44100%
ClearAllTransientState()100%44100%
RecordData(...)100%44100%
RecordOptionalLocomotion(...)100%22100%

File(s)

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

#LineLine coverage
 1//=======================================================================
 2// LocomotionHandler.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.Collections.Generic;
 10using System.Diagnostics.CodeAnalysis;
 11using System.Runtime.CompilerServices;
 12using Chronicler;
 13
 14namespace Trailblazer.Navigation.Motor;
 15
 16/// <summary>
 17/// Manages locomotion states and behaviors for the <see cref="NavMotor"/>.
 18/// </summary>
 19/// <remarks>
 20/// This class coordinates multiple locomotion types, ensuring that movement states are properly managed.
 21/// </remarks>
 22public class LocomotionHandler : IRecordable
 23{
 24    #region Nested Types
 25
 26    private enum LocomotionSlot
 27    {
 28        Move,
 29        Platform,
 30        Jump,
 31        Fall,
 32        Slide,
 33        Water,
 34        Fly,
 35        Climb
 36    }
 37
 38    #endregion
 39
 40    #region Fields
 41
 42    /// <summary>
 43    /// Determines whether the scout has control over movement input.
 44    /// </summary>
 110145    public bool IsInControl = true;
 46
 110147    private LocomotionForces _forces = new();
 48
 49    private MoveLocomotion _move = null!;
 50
 51    private FallLocomotion _fall = null!;
 52
 53    private PlatformLocomotion _platform = null!;
 54
 55    private JumpLocomotion? _jump;
 56
 57    private SlideLocomotion? _slide;
 58
 59    private WaterLocomotion? _water;
 60
 61    private FlyLocomotion? _fly;
 62
 63    private ClimbLocomotion? _climb;
 64
 65    private TrailblazerWorldContext? _context;
 66
 67    #endregion
 68
 69    #region Initialization
 70
 71    /// <summary>
 72    /// Initializes a new handler using the default locomotion profile.
 73    /// </summary>
 74    public LocomotionHandler()
 155875        : this(LocomotionProfile.CreateDefault()) { }
 76
 77    /// <summary>
 78    /// Initializes a new handler from a locomotion profile.
 79    /// </summary>
 110180    public LocomotionHandler(LocomotionProfile profile)
 81    {
 110182        ApplyProfile(profile ?? throw new ArgumentNullException(nameof(profile)));
 110083    }
 84
 85    #endregion
 86
 87    #region Properties
 88
 89    /// <summary>
 90    /// Gets the currently installed locomotion kinds.
 91    /// </summary>
 92    public LocomotionKind InstalledKinds { get; private set; } = LocomotionKind.All;
 93
 94    /// <inheritdoc cref="LocomotionForces"/>
 389895    public LocomotionForces Forces => _forces;
 96
 97    /// <summary>
 98    /// Handles general movement, including speed limits, acceleration, and velocity calculations.
 99    /// </summary>
 13564100    public MoveLocomotion Move => _move;
 101
 102    /// <summary>
 103    /// Manages movement when interacting with moving platforms or surfaces.
 104    /// </summary>
 105    /// <remarks>
 106    /// This locomotion maintains platform velocity tracking and movement transfer states.
 107    /// </remarks>
 14474108    public PlatformLocomotion Platform => _platform;
 109
 110    /// <summary>
 111    /// Controls the airborne state when a jump is executed successfully.
 112    /// </summary>
 113    /// <remarks>
 114    /// This locomotion governs jump height, cooldown timing, and jump force calculations.
 115    /// </remarks>
 20803116    public JumpLocomotion? Jump => _jump;
 117
 118    /// <summary>
 119    /// Handles the scout’s falling behavior when downward momentum is detected.
 120    /// </summary>
 121    /// <remarks>
 122    /// This locomotion tracks fall distance, applies landing impact logic, and determines if a scout is free-falling.
 123    /// </remarks>
 14722124    public FallLocomotion Fall => _fall;
 125
 126    /// <summary>
 127    /// Manages movement when sliding down steep surfaces.
 128    /// </summary>
 129    /// <remarks>
 130    /// This locomotion determines when the scout should slide and how much control it has over movement during the slid
 131    /// </remarks>
 5814132    public SlideLocomotion? Slide => _slide;
 133
 134    /// <summary>
 135    /// Handles movement when the scout is in water, including active swimming, buoyancy, and water resistance.
 136    /// </summary>
 137    /// <remarks>
 138    /// This locomotion tracks liquid-medium state such as swim speed, floating and sinking behavior,
 139    /// dive time, and breath management.
 140    /// </remarks>
 11473141    public WaterLocomotion? Water => _water;
 142
 143    /// <summary>
 144    /// Handles controlled flight while the scout is airborne.
 145    /// </summary>
 26438146    public FlyLocomotion? Fly => _fly;
 147
 148    /// <summary>
 149    /// Handles climb configuration and runtime attachment state.
 150    /// </summary>
 27617151    public ClimbLocomotion? Climb => _climb;
 152
 153    #endregion
 154
 155    #region Composition
 156
 157    /// <summary>
 158    /// Gets whether a built-in locomotion kind is installed.
 159    /// </summary>
 160    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 161    public bool Has(LocomotionKind kind)
 162    {
 2163        return (InstalledKinds & kind) == kind;
 164    }
 165
 166    /// <summary>
 167    /// Gets whether a built-in locomotion type is installed.
 168    /// </summary>
 169    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 170    public bool Has<T>() where T : class, ILocomotion
 171    {
 3172        return TryGet<T>(out _);
 173    }
 174
 175    /// <summary>
 176    /// Attempts to retrieve an installed locomotion by type.
 177    /// </summary>
 178    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 179    public bool TryGet<T>([NotNullWhen(true)] out T? locomotion) where T : class, ILocomotion
 180    {
 9181        locomotion = GetLocomotion(typeof(T)) as T;
 9182        return locomotion != null;
 183    }
 184
 185    /// <summary>
 186    /// Retrieves an installed locomotion by type or throws if it is not installed.
 187    /// </summary>
 188    public T Require<T>() where T : class, ILocomotion
 189    {
 3190        if (TryGet<T>(out T? locomotion))
 1191            return locomotion;
 192
 2193        throw new InvalidOperationException($"{typeof(T).Name} is not installed on this locomotion handler.");
 194    }
 195
 196    /// <summary>
 197    /// Installs or replaces a locomotion instance.
 198    /// </summary>
 199    public void Install<T>(T locomotion) where T : class, ILocomotion
 200    {
 3201        Replace(locomotion);
 3202    }
 203
 204    /// <summary>
 205    /// Replaces a locomotion instance with a new one of the same type.
 206    /// </summary>
 207    public void Replace<T>(T locomotion) where T : class, ILocomotion
 208    {
 13209        SwiftThrowHelper.ThrowIfNull(locomotion, nameof(locomotion));
 210
 12211        SetLocomotion(typeof(T), locomotion);
 11212        RefreshInstalledKinds();
 11213    }
 214
 215    /// <summary>
 216    /// Removes an optional locomotion from the handler.
 217    /// </summary>
 218    public bool Remove<T>() where T : class, ILocomotion
 219    {
 7220        Type type = typeof(T);
 7221        if (type == typeof(MoveLocomotion)
 7222            || type == typeof(PlatformLocomotion)
 7223            || type == typeof(FallLocomotion))
 224        {
 3225            return false;
 226        }
 227
 4228        ILocomotion? locomotion = GetLocomotion(type);
 4229        if (locomotion == null)
 1230            return false;
 231
 3232        locomotion.ClearTransientState();
 233
 3234        SetLocomotion(type, null);
 3235        RefreshInstalledKinds();
 3236        return true;
 237    }
 238
 239    /// <summary>
 240    /// Replaces the installed locomotions with a new profile.
 241    /// </summary>
 242    public void ApplyProfile(LocomotionProfile profile)
 243    {
 1266244        SwiftThrowHelper.ThrowIfNull(profile, nameof(profile));
 245
 1265246        ClearReplacedLocomotion(Move, profile.Move);
 1265247        ClearReplacedLocomotion(Fall, profile.Fall);
 1265248        ClearReplacedLocomotion(Platform, profile.Platform);
 1265249        ClearReplacedLocomotion(Jump, profile.Jump);
 1265250        ClearReplacedLocomotion(Slide, profile.Slide);
 1265251        ClearReplacedLocomotion(Water, profile.Water);
 1265252        ClearReplacedLocomotion(Fly, profile.Fly);
 1265253        ClearReplacedLocomotion(Climb, profile.Climb);
 254
 1265255        _move = profile.Move;
 1265256        _fall = profile.Fall;
 1265257        _platform = profile.Platform;
 1265258        _jump = profile.Jump;
 1265259        _slide = profile.Slide;
 1265260        _water = profile.Water;
 1265261        _fly = profile.Fly;
 1265262        _climb = profile.Climb;
 263
 1265264        BindInstalledLocomotions();
 1265265        RefreshInstalledKinds();
 1265266    }
 267
 268    internal void BindContext(TrailblazerWorldContext context)
 269    {
 1118270        TrailblazerWorldContext.ThrowIfUnusable(context);
 1118271        _context = context;
 1118272        BindInstalledLocomotions();
 1118273    }
 274
 275    /// <summary>
 276    /// Creates a profile representing the handler's current locomotion composition.
 277    /// </summary>
 278    public LocomotionProfile ToProfile()
 279    {
 1280        return new LocomotionProfile(
 1281            Move,
 1282            Fall,
 1283            Platform,
 1284            Jump,
 1285            Slide,
 1286            Water,
 1287            Fly,
 1288            Climb);
 289    }
 290
 291    internal void ConfigureInstalledKinds(LocomotionKind kinds)
 292    {
 80293        LocomotionKind normalizedKinds = kinds | LocomotionKind.Core;
 80294        var builder = new LocomotionProfileBuilder(includeOptionalLocomotions: false);
 295
 80296        if ((normalizedKinds & LocomotionKind.Jump) != 0)
 74297            builder.WithJump();
 298
 80299        if ((normalizedKinds & LocomotionKind.Slide) != 0)
 74300            builder.WithSlide();
 301
 80302        if ((normalizedKinds & LocomotionKind.Water) != 0)
 74303            builder.WithWater();
 304
 80305        if ((normalizedKinds & LocomotionKind.Fly) != 0)
 75306            builder.WithFly();
 307
 80308        if ((normalizedKinds & LocomotionKind.Climb) != 0)
 75309            builder.WithClimb();
 310
 80311        ApplyProfile(builder.Build());
 80312    }
 313
 314    private void RefreshInstalledKinds()
 315    {
 1279316        InstalledKinds = LocomotionKind.Core;
 317
 1279318        if (Jump != null)
 1230319            InstalledKinds |= LocomotionKind.Jump;
 320
 1279321        if (Slide != null)
 1222322            InstalledKinds |= LocomotionKind.Slide;
 323
 1279324        if (Water != null)
 1224325            InstalledKinds |= LocomotionKind.Water;
 326
 1279327        if (Fly != null)
 1225328            InstalledKinds |= LocomotionKind.Fly;
 329
 1279330        if (Climb != null)
 1224331            InstalledKinds |= LocomotionKind.Climb;
 1279332    }
 333
 334    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 335    private ILocomotion? GetLocomotion(Type type)
 336    {
 64337        if (!TryResolveLocomotionSlot(type, out LocomotionSlot slot))
 3338            return null;
 339
 61340        return GetLocomotion(slot);
 341    }
 342
 343    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 344    private ILocomotion? GetLocomotion(LocomotionSlot slot)
 345    {
 67346        if (slot == LocomotionSlot.Move) return Move;
 56347        if (slot == LocomotionSlot.Platform) return Platform;
 59348        if (slot == LocomotionSlot.Jump) return Jump;
 65349        if (slot == LocomotionSlot.Fall) return Fall;
 35350        if (slot == LocomotionSlot.Slide) return Slide;
 40351        if (slot == LocomotionSlot.Water) return Water;
 24352        if (slot == LocomotionSlot.Fly) return Fly;
 20353        return Climb;
 354    }
 355
 356    /// <summary>
 357    /// Gets and enumerates all locomotion instances in the handler.
 358    /// </summary>
 359    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 360    public IEnumerable<ILocomotion> GetLocomotions()
 361    {
 13362        yield return Move;
 13363        yield return Platform;
 364
 13365        if (Jump != null)
 12366            yield return Jump;
 367
 13368        yield return Fall;
 369
 13370        if (Water != null)
 12371            yield return Water;
 372
 13373        if (Fly != null)
 12374            yield return Fly;
 375
 13376        if (Climb != null)
 12377            yield return Climb;
 378
 13379        if (Slide != null)
 12380            yield return Slide;
 13381    }
 382
 383    private void SetLocomotion(Type type, ILocomotion? locomotion)
 384    {
 15385        if (!TryResolveLocomotionSlot(type, out LocomotionSlot slot))
 1386            throw new NotSupportedException($"Unsupported locomotion type '{type.Name}'.");
 387
 14388        SetLocomotion(slot, locomotion);
 14389    }
 390
 391    private static bool TryResolveLocomotionSlot(Type type, out LocomotionSlot slot)
 392    {
 79393        if (type == typeof(MoveLocomotion))
 394        {
 7395            slot = LocomotionSlot.Move;
 7396            return true;
 397        }
 398
 72399        if (type == typeof(PlatformLocomotion))
 400        {
 2401            slot = LocomotionSlot.Platform;
 2402            return true;
 403        }
 404
 70405        if (type == typeof(JumpLocomotion))
 406        {
 8407            slot = LocomotionSlot.Jump;
 8408            return true;
 409        }
 410
 62411        if (type == typeof(FallLocomotion))
 412        {
 17413            slot = LocomotionSlot.Fall;
 17414            return true;
 415        }
 416
 45417        if (type == typeof(SlideLocomotion))
 418        {
 4419            slot = LocomotionSlot.Slide;
 4420            return true;
 421        }
 422
 41423        if (type == typeof(WaterLocomotion))
 424        {
 12425            slot = LocomotionSlot.Water;
 12426            return true;
 427        }
 428
 29429        if (type == typeof(FlyLocomotion))
 430        {
 3431            slot = LocomotionSlot.Fly;
 3432            return true;
 433        }
 434
 26435        if (type == typeof(ClimbLocomotion))
 436        {
 22437            slot = LocomotionSlot.Climb;
 22438            return true;
 439        }
 440
 4441        slot = default;
 4442        return false;
 443    }
 444
 445    private void SetLocomotion(LocomotionSlot slot, ILocomotion? locomotion)
 446    {
 14447        BindLocomotion(locomotion);
 448
 449        switch (slot)
 450        {
 451            case LocomotionSlot.Move:
 1452                _move = (MoveLocomotion)locomotion!;
 1453                return;
 454            case LocomotionSlot.Platform:
 1455                _platform = (PlatformLocomotion)locomotion!;
 1456                return;
 457            case LocomotionSlot.Jump:
 3458                _jump = locomotion as JumpLocomotion;
 3459                return;
 460            case LocomotionSlot.Fall:
 1461                _fall = (FallLocomotion)locomotion!;
 1462                return;
 463            case LocomotionSlot.Slide:
 2464                _slide = locomotion as SlideLocomotion;
 2465                return;
 466            case LocomotionSlot.Water:
 3467                _water = locomotion as WaterLocomotion;
 3468                return;
 469            case LocomotionSlot.Fly:
 1470                _fly = locomotion as FlyLocomotion;
 1471                return;
 472        }
 473
 2474        _climb = locomotion as ClimbLocomotion;
 2475    }
 476
 477    private void BindInstalledLocomotions()
 478    {
 2383479        BindLocomotion(_move);
 2383480        BindLocomotion(_platform);
 2383481        BindLocomotion(_jump);
 2383482        BindLocomotion(_fall);
 2383483        BindLocomotion(_slide);
 2383484        BindLocomotion(_water);
 2383485        BindLocomotion(_fly);
 2383486        BindLocomotion(_climb);
 2383487    }
 488
 489    private void BindLocomotion(ILocomotion? locomotion)
 490    {
 19078491        if (_context == null || locomotion == null)
 9027492            return;
 493
 494        switch (locomotion)
 495        {
 496            case JumpLocomotion jump:
 1244497                jump.BindContext(_context);
 1244498                break;
 499            case PlatformLocomotion platform:
 1280500                platform.BindContext(_context);
 1280501                break;
 502            case WaterLocomotion water:
 1241503                water.BindContext(_context);
 504                break;
 505        }
 1241506    }
 507
 508    private static void ClearReplacedLocomotion(ILocomotion? current, ILocomotion? next)
 509    {
 10120510        if (current == null || ReferenceEquals(current, next))
 9453511            return;
 512
 667513        current.ClearTransientState();
 667514    }
 515
 516    #endregion
 517
 518    #region Transient State Management
 519
 520    /// <summary>
 521    /// Synchronizes locomotion states with another <see cref="LocomotionHandler"/> instance.
 522    /// </summary>
 523    /// <remarks>
 524    /// This ensures that all locomotion modules maintain consistent movement behavior when synchronizing states,
 525    /// which is useful for rollback systems or deterministic simulations.
 526    /// </remarks>
 527    /// <param name="other">The locomotion handler instance to sync with.</param>
 528    public void SyncTransientState(LocomotionHandler other)
 529    {
 3530        if (other == null) return;
 531
 1532        IsInControl = other.IsInControl;
 533
 18534        foreach (var locomotion in GetLocomotions())
 535        {
 8536            if (!locomotion.IsEnabled) continue;
 7537            ILocomotion? otherLocomotion = other.GetLocomotion(locomotion.GetType());
 7538            if (otherLocomotion == null) continue;
 3539            locomotion.SyncTransientState(otherLocomotion);
 540        }
 1541    }
 542
 543    /// <summary>
 544    /// Clears the transient state for the locomotion component of the specified type, if it is enabled.
 545    /// </summary>
 546    /// <remarks>This method has no effect if the specified locomotion component is not present or is not enabled.</rema
 547    /// <typeparam name="T">The type of locomotion component for which to clear transient state. Must implement the ILoc
 548    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 549    public void ClearTransientState<T>() where T : ILocomotion
 550    {
 44551        var locomotion = GetLocomotion(typeof(T));
 44552        if (locomotion != null && locomotion.IsEnabled)
 42553            locomotion.ClearTransientState();
 44554    }
 555
 556    /// <summary>
 557    /// Clears the transient state of all locomotion modules.
 558    /// </summary>
 559    /// <remarks>
 560    /// This method resets movement states without altering locomotion configurations,
 561    /// ensuring a clean reset of position, velocity, and state-based properties.
 562    /// </remarks>
 563    public void ClearAllTransientState()
 564    {
 180565        foreach (var locomotion in GetLocomotions())
 566        {
 80567            if (locomotion.IsEnabled)
 80568                locomotion.ClearTransientState();
 569        }
 10570    }
 571
 572    #endregion
 573
 574    #region Serialization
 575
 576    /// <inheritdoc />
 577    public void RecordData(IChronicler chronicler)
 578    {
 164579        RecordValues.Look(chronicler, ref IsInControl, "IsInControl", true);
 164580        int installedKinds = (int)InstalledKinds;
 164581        RecordValues.Look(chronicler, ref installedKinds, "InstalledKinds", (int)LocomotionKind.All);
 582
 164583        if (chronicler.Mode == SerializationMode.Loading)
 77584            ConfigureInstalledKinds((LocomotionKind)installedKinds);
 585
 164586        RecordDeep.Look(chronicler, ref _forces, "Forces");
 164587        RecordDeep.Look(chronicler, ref _move, "Move");
 164588        RecordDeep.Look(chronicler, ref _platform, "Platform");
 164589        RecordOptionalLocomotion(chronicler, ref _jump, "Jump");
 164590        RecordDeep.Look(chronicler, ref _fall, "Fall");
 164591        RecordOptionalLocomotion(chronicler, ref _slide, "Slide");
 164592        RecordOptionalLocomotion(chronicler, ref _water, "Water");
 164593        RecordOptionalLocomotion(chronicler, ref _fly, "Fly");
 164594        RecordOptionalLocomotion(chronicler, ref _climb, "Climb");
 595
 164596        if (chronicler.Mode == SerializationMode.Loading)
 597        {
 77598            ApplyProfile(new LocomotionProfile(
 77599                _move,
 77600                _fall,
 77601                _platform,
 77602                _jump,
 77603                _slide,
 77604                _water,
 77605                _fly,
 77606                _climb));
 607        }
 164608    }
 609
 610    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 611    private static void RecordOptionalLocomotion<T>(
 612        IChronicler chronicler,
 613        ref T? locomotion,
 614        string id)
 615        where T : class, ILocomotion, IRecordable
 616    {
 820617        T local = locomotion ?? null!;
 820618        RecordDeep.Look(chronicler, ref local, id);
 820619        locomotion = local;
 820620    }
 621
 622    #endregion
 623}