diff --git a/src/Application/CTF.Application.csproj b/src/Application/CTF.Application.csproj index 7d5bf1dc..7be5084b 100644 --- a/src/Application/CTF.Application.csproj +++ b/src/Application/CTF.Application.csproj @@ -1,12 +1,13 @@  - + true + @@ -29,4 +30,8 @@ + + + + diff --git a/src/Application/Common/Resources/Messages.Designer.cs b/src/Application/Common/Resources/Messages.Designer.cs index ad17f5f2..2bc82903 100644 --- a/src/Application/Common/Resources/Messages.Designer.cs +++ b/src/Application/Common/Resources/Messages.Designer.cs @@ -78,6 +78,24 @@ internal static string EmptyWeaponPackage { } } + /// + /// Looks up a localized string similar to The interval must be between 0 to {Max}. + /// + internal static string InvalidInterval { + get { + return ResourceManager.GetString("InvalidInterval", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid map id has been passed. + /// + internal static string InvalidMap { + get { + return ResourceManager.GetString("InvalidMap", resourceCulture); + } + } + /// /// Looks up a localized string similar to Must be 3-20 characters long and only contain valid characters (0-9, a-z, A-Z, [], (), $, @ . _ and = only). /// @@ -132,6 +150,24 @@ internal static string InvalidWeapon { } } + /// + /// Looks up a localized string similar to The spawn location list cannot be empty. + /// + internal static string LocationListCannotBeEmpty { + get { + return ResourceManager.GetString("LocationListCannotBeEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Map has not been found. + /// + internal static string MapNotFound { + get { + return ResourceManager.GetString("MapNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Player '{Name}' is a member that already exists. /// @@ -186,6 +222,15 @@ internal static string PlayerNotFound { } } + /// + /// Looks up a localized string similar to A spawn location can only be obtained for the alpha or beta team. + /// + internal static string SpawnLocationFailure { + get { + return ResourceManager.GetString("SpawnLocationFailure", resourceCulture); + } + } + /// /// Looks up a localized string similar to Points must be between -1 to -100. /// diff --git a/src/Application/Common/Resources/Messages.resx b/src/Application/Common/Resources/Messages.resx index f70b5d9a..7dff94a2 100644 --- a/src/Application/Common/Resources/Messages.resx +++ b/src/Application/Common/Resources/Messages.resx @@ -123,6 +123,12 @@ You have no items in your weapon package + + The interval must be between 0 to {Max} + + + Invalid map id has been passed + Must be 3-20 characters long and only contain valid characters (0-9, a-z, A-Z, [], (), $, @ . _ and = only) @@ -141,6 +147,12 @@ Invalid weapon has been passed + + The spawn location list cannot be empty + + + Map has not been found + Player '{Name}' is a member that already exists @@ -159,6 +171,9 @@ Player '{Name}' is not found + + A spawn location can only be obtained for the alpha or beta team + Points must be between -1 to -100 diff --git a/src/Application/Common/Result.cs b/src/Application/Common/Result.cs index 8d2b669d..6faf606a 100644 --- a/src/Application/Common/Result.cs +++ b/src/Application/Common/Result.cs @@ -3,32 +3,32 @@ /// /// Represents the result of an operation that does not return a value. /// -public readonly ref struct Result +public ref struct Result { public Result() { } /// /// Gets the description of a result. /// - public string Message { get; init; } = string.Empty; + public string Message { get; private set; } = string.Empty; /// /// A value indicating that the result was successful. /// - public bool IsSuccess { get; init; } = true; + public bool IsSuccess { get; private set; } = false; /// /// A value that indicates that the result was a failure. /// public bool IsFailed => !IsSuccess; - public static Result Success() => new(); - public static Result Success(string message) => new() { Message = message }; - - public static Result Failure() => new() { IsSuccess = false }; - public static Result Failure(string message) => new() - { - IsSuccess = false, - Message = message + public static Result Success() => new() { IsSuccess = true }; + public static Result Success(string message) => new() + { + IsSuccess = true, + Message = message }; + + public static Result Failure() => new(); + public static Result Failure(string message) => new() { Message = message }; } diff --git a/src/Application/Common/ResultOfT.cs b/src/Application/Common/ResultOfT.cs index f3b5ff5f..cbba91e7 100644 --- a/src/Application/Common/ResultOfT.cs +++ b/src/Application/Common/ResultOfT.cs @@ -4,41 +4,59 @@ /// Represents the result of an operation that returns a value. /// /// A value associated to the result. -public readonly ref struct Result +public ref struct Result { + private TValue _value; public Result() { } /// /// Gets the value associated with the result. /// - public TValue Value { get; init; } = default; + /// + /// when is true. + /// + public TValue Value + { + get + { + return IsSuccess ? + _value : + throw new InvalidOperationException("The value of a failure result can not be accessed."); + } + private set + { + _value = value; + } + } /// /// Gets the description of a result. /// - public string Message { get; init; } = string.Empty; + public string Message { get; private set; } = string.Empty; /// /// A value indicating that the result was successful. /// - public bool IsSuccess { get; init; } = true; + public bool IsSuccess { get; private set; } = false; /// /// A value that indicates that the result was a failure. /// public bool IsFailed => !IsSuccess; - public static Result Success(TValue value) => new() { Value = value }; + public static Result Success(TValue value) => new() + { + IsSuccess = true, + Value = value + }; + public static Result Success(TValue value, string message) => new() { + IsSuccess = true, Value = value, Message = message }; - public static Result Failure() => new() { IsSuccess = false }; - public static Result Failure(string message) => new() - { - IsSuccess = false, - Message = message - }; + public static Result Failure() => new(); + public static Result Failure(string message) => new() { Message = message }; } diff --git a/src/Application/Maps/CurrentMap.cs b/src/Application/Maps/CurrentMap.cs new file mode 100644 index 00000000..6f41b42b --- /dev/null +++ b/src/Application/Maps/CurrentMap.cs @@ -0,0 +1,65 @@ +namespace CTF.Application.Maps; + +/// +/// Represents the current information of a map. +/// +public class CurrentMap +{ + private IMap _nextMap; + private readonly Random _random = new(); + public const int DefaultInterior = 0; + public const int DefaultWeather = 10; + public const int DefaultWorldTime = 12; + public int Id { get; } + public string Name { get; } + public IReadOnlyList AlphaTeamLocations { get; } + public IReadOnlyList BetaTeamLocations { get; } + public int Interior { get; } + public int Weather { get; } + public int WorldTime { get; } + public IMap NextMap => _nextMap; + public int IsLoading { get; set; } + + public CurrentMap( + IMap map, + IReadOnlyList alphaTeamLocations, + IReadOnlyList betaTeamLocations, + int interior = DefaultInterior, + int weather = DefaultWeather, + int worldTime = DefaultWorldTime) + { + ArgumentNullException.ThrowIfNull(map); + ArgumentNullException.ThrowIfNull(alphaTeamLocations); + ArgumentNullException.ThrowIfNull(betaTeamLocations); + + if (alphaTeamLocations.Count == 0) + throw new ArgumentException(Messages.LocationListCannotBeEmpty, nameof(alphaTeamLocations)); + + if (betaTeamLocations.Count == 0) + throw new ArgumentException(Messages.LocationListCannotBeEmpty, nameof(betaTeamLocations)); + + Id = map.Id; + Name = map.Name; + AlphaTeamLocations = alphaTeamLocations; + BetaTeamLocations = betaTeamLocations; + Interior = interior; + Weather = weather; + WorldTime = worldTime; + int nextMapId = (Id + 1) % MapCollection.Count; + _nextMap = MapCollection.GetById(nextMapId).Value; + } + + public string GetMapNameAsText() => $"Map: ~w~{Name}"; + public void SetNextMap(IMap nextMap) + { + ArgumentNullException.ThrowIfNull(nextMap); + _nextMap = nextMap; + } + + public SpawnLocation GetRandomSpawnLocation(TeamId team) => team switch + { + TeamId.Alpha => AlphaTeamLocations[_random.Next(AlphaTeamLocations.Count)], + TeamId.Beta => BetaTeamLocations[_random.Next(BetaTeamLocations.Count)], + _ => throw new NotSupportedException(Messages.SpawnLocationFailure) + }; +} diff --git a/scriptfiles/maps/Aim_Headshot.ini b/src/Application/Maps/Files/Aim_Headshot.ini similarity index 100% rename from scriptfiles/maps/Aim_Headshot.ini rename to src/Application/Maps/Files/Aim_Headshot.ini diff --git a/scriptfiles/maps/Aim_Headshot2.ini b/src/Application/Maps/Files/Aim_Headshot2.ini similarity index 100% rename from scriptfiles/maps/Aim_Headshot2.ini rename to src/Application/Maps/Files/Aim_Headshot2.ini diff --git a/scriptfiles/maps/Area51.ini b/src/Application/Maps/Files/Area51.ini similarity index 100% rename from scriptfiles/maps/Area51.ini rename to src/Application/Maps/Files/Area51.ini diff --git a/scriptfiles/maps/Area66.ini b/src/Application/Maps/Files/Area66.ini similarity index 100% rename from scriptfiles/maps/Area66.ini rename to src/Application/Maps/Files/Area66.ini diff --git a/scriptfiles/maps/Compound.ini b/src/Application/Maps/Files/Compound.ini similarity index 100% rename from scriptfiles/maps/Compound.ini rename to src/Application/Maps/Files/Compound.ini diff --git a/scriptfiles/maps/CrackFactory.ini b/src/Application/Maps/Files/CrackFactory.ini similarity index 100% rename from scriptfiles/maps/CrackFactory.ini rename to src/Application/Maps/Files/CrackFactory.ini diff --git a/scriptfiles/maps/DesertGlory.ini b/src/Application/Maps/Files/DesertGlory.ini similarity index 100% rename from scriptfiles/maps/DesertGlory.ini rename to src/Application/Maps/Files/DesertGlory.ini diff --git a/scriptfiles/maps/GateToHell.ini b/src/Application/Maps/Files/GateToHell.ini similarity index 100% rename from scriptfiles/maps/GateToHell.ini rename to src/Application/Maps/Files/GateToHell.ini diff --git a/scriptfiles/maps/RC_Battlefield.ini b/src/Application/Maps/Files/RC_Battlefield.ini similarity index 100% rename from scriptfiles/maps/RC_Battlefield.ini rename to src/Application/Maps/Files/RC_Battlefield.ini diff --git a/scriptfiles/maps/SA_Hill.ini b/src/Application/Maps/Files/SA_Hill.ini similarity index 100% rename from scriptfiles/maps/SA_Hill.ini rename to src/Application/Maps/Files/SA_Hill.ini diff --git a/scriptfiles/maps/Simpson.ini b/src/Application/Maps/Files/Simpson.ini similarity index 100% rename from scriptfiles/maps/Simpson.ini rename to src/Application/Maps/Files/Simpson.ini diff --git a/scriptfiles/maps/TheBunker.ini b/src/Application/Maps/Files/TheBunker.ini similarity index 100% rename from scriptfiles/maps/TheBunker.ini rename to src/Application/Maps/Files/TheBunker.ini diff --git a/scriptfiles/maps/TheConstruction.ini b/src/Application/Maps/Files/TheConstruction.ini similarity index 100% rename from scriptfiles/maps/TheConstruction.ini rename to src/Application/Maps/Files/TheConstruction.ini diff --git a/scriptfiles/maps/TheWild.ini b/src/Application/Maps/Files/TheWild.ini similarity index 100% rename from scriptfiles/maps/TheWild.ini rename to src/Application/Maps/Files/TheWild.ini diff --git a/scriptfiles/maps/WarZone.ini b/src/Application/Maps/Files/WarZone.ini similarity index 100% rename from scriptfiles/maps/WarZone.ini rename to src/Application/Maps/Files/WarZone.ini diff --git a/scriptfiles/maps/ZM_Italy.ini b/src/Application/Maps/Files/ZM_Italy.ini similarity index 100% rename from scriptfiles/maps/ZM_Italy.ini rename to src/Application/Maps/Files/ZM_Italy.ini diff --git a/scriptfiles/maps/cs_assault.ini b/src/Application/Maps/Files/cs_assault.ini similarity index 100% rename from scriptfiles/maps/cs_assault.ini rename to src/Application/Maps/Files/cs_assault.ini diff --git a/scriptfiles/maps/cs_deagle5.ini b/src/Application/Maps/Files/cs_deagle5.ini similarity index 100% rename from scriptfiles/maps/cs_deagle5.ini rename to src/Application/Maps/Files/cs_deagle5.ini diff --git a/scriptfiles/maps/cs_rockwar.ini b/src/Application/Maps/Files/cs_rockwar.ini similarity index 100% rename from scriptfiles/maps/cs_rockwar.ini rename to src/Application/Maps/Files/cs_rockwar.ini diff --git a/scriptfiles/maps/de_aztec.ini b/src/Application/Maps/Files/de_aztec.ini similarity index 100% rename from scriptfiles/maps/de_aztec.ini rename to src/Application/Maps/Files/de_aztec.ini diff --git a/scriptfiles/maps/de_dust2.ini b/src/Application/Maps/Files/de_dust2.ini similarity index 100% rename from scriptfiles/maps/de_dust2.ini rename to src/Application/Maps/Files/de_dust2.ini diff --git a/scriptfiles/maps/de_dust2_small.ini b/src/Application/Maps/Files/de_dust2_small.ini similarity index 100% rename from scriptfiles/maps/de_dust2_small.ini rename to src/Application/Maps/Files/de_dust2_small.ini diff --git a/scriptfiles/maps/de_dust2_x1.ini b/src/Application/Maps/Files/de_dust2_x1.ini similarity index 100% rename from scriptfiles/maps/de_dust2_x1.ini rename to src/Application/Maps/Files/de_dust2_x1.ini diff --git a/scriptfiles/maps/de_dust2_x2.ini b/src/Application/Maps/Files/de_dust2_x2.ini similarity index 100% rename from scriptfiles/maps/de_dust2_x2.ini rename to src/Application/Maps/Files/de_dust2_x2.ini diff --git a/scriptfiles/maps/de_dust2_x3.ini b/src/Application/Maps/Files/de_dust2_x3.ini similarity index 100% rename from scriptfiles/maps/de_dust2_x3.ini rename to src/Application/Maps/Files/de_dust2_x3.ini diff --git a/scriptfiles/maps/de_dust5.ini b/src/Application/Maps/Files/de_dust5.ini similarity index 100% rename from scriptfiles/maps/de_dust5.ini rename to src/Application/Maps/Files/de_dust5.ini diff --git a/scriptfiles/maps/fy_iceworld.ini b/src/Application/Maps/Files/fy_iceworld.ini similarity index 100% rename from scriptfiles/maps/fy_iceworld.ini rename to src/Application/Maps/Files/fy_iceworld.ini diff --git a/scriptfiles/maps/fy_iceworld2.ini b/src/Application/Maps/Files/fy_iceworld2.ini similarity index 100% rename from scriptfiles/maps/fy_iceworld2.ini rename to src/Application/Maps/Files/fy_iceworld2.ini diff --git a/scriptfiles/maps/fy_snow.ini b/src/Application/Maps/Files/fy_snow.ini similarity index 100% rename from scriptfiles/maps/fy_snow.ini rename to src/Application/Maps/Files/fy_snow.ini diff --git a/scriptfiles/maps/fy_snow2.ini b/src/Application/Maps/Files/fy_snow2.ini similarity index 100% rename from scriptfiles/maps/fy_snow2.ini rename to src/Application/Maps/Files/fy_snow2.ini diff --git a/scriptfiles/maps/mp_island.ini b/src/Application/Maps/Files/mp_island.ini similarity index 100% rename from scriptfiles/maps/mp_island.ini rename to src/Application/Maps/Files/mp_island.ini diff --git a/scriptfiles/maps/mp_jetdoor.ini b/src/Application/Maps/Files/mp_jetdoor.ini similarity index 100% rename from scriptfiles/maps/mp_jetdoor.ini rename to src/Application/Maps/Files/mp_jetdoor.ini diff --git a/scriptfiles/maps/zone_paintball.ini b/src/Application/Maps/Files/zone_paintball.ini similarity index 100% rename from scriptfiles/maps/zone_paintball.ini rename to src/Application/Maps/Files/zone_paintball.ini diff --git a/src/Application/Maps/IMap.cs b/src/Application/Maps/IMap.cs new file mode 100644 index 00000000..f011dccd --- /dev/null +++ b/src/Application/Maps/IMap.cs @@ -0,0 +1,7 @@ +namespace CTF.Application.Maps; + +public interface IMap +{ + int Id { get; } + string Name { get; } +} diff --git a/src/Application/Maps/LoadTime.cs b/src/Application/Maps/LoadTime.cs new file mode 100644 index 00000000..97b21a33 --- /dev/null +++ b/src/Application/Maps/LoadTime.cs @@ -0,0 +1,58 @@ +namespace CTF.Application.Maps; + +/// +/// Represents the total wait time for the new map to load. +/// +public class LoadTime +{ + private readonly Action _onLoadingMap; + private readonly Action _onLoadedMap; + private int _interval = MaxLoadTime; + public const int MaxLoadTime = 6; + + /// + /// Displays the load time in the game. + /// + public string GameText { get; private set; } = string.Empty; + + /// + /// Represents the interval in seconds. + /// + public int Interval => _interval; + + public LoadTime(Action onLoadingMap, Action onLoadedMap) + { + ArgumentNullException.ThrowIfNull(onLoadingMap); + ArgumentNullException.ThrowIfNull(onLoadedMap); + _onLoadingMap = onLoadingMap; + _onLoadedMap = onLoadedMap; + } + + /// + /// Reduces the load time until it reaches zero. + /// + public void Decrease() + { + if (_interval == 0) + { + Reset(); + _onLoadedMap(); + return; + } + + if (_interval == MaxLoadTime) + { + _onLoadingMap(); + } + + _interval--; + UpdateGameText(); + } + + private void UpdateGameText() => GameText = $"Loading map... ({_interval})"; + private void Reset() + { + _interval = MaxLoadTime; + GameText = string.Empty; + } +} diff --git a/src/Application/Maps/MapCollection.cs b/src/Application/Maps/MapCollection.cs new file mode 100644 index 00000000..e10a5aa7 --- /dev/null +++ b/src/Application/Maps/MapCollection.cs @@ -0,0 +1,59 @@ +namespace CTF.Application.Maps; + +public class MapCollection +{ + private static Map[] s_maps; + static MapCollection() => SetMapNamesFromFileSystem(); + private static void SetMapNamesFromFileSystem() + { + var path = Path.Combine(AppContext.BaseDirectory, "Maps", "Files"); + var random = new Random(); + string[] names = Directory.GetFiles(path); + random.Shuffle(names); + s_maps = new Map[names.Length]; + for (int i = 0; i < names.Length; i++) + { + var map = new Map + { + Id = i, + Name = Path.GetFileNameWithoutExtension(names[i]) + }; + s_maps[i] = map; + } + } + + public static int Count => s_maps.Length; + public static IReadOnlyList GetAll() => s_maps; + public static IEnumerable GetAll(string findBy) + { + foreach(Map map in s_maps) + { + if (map.Name.StartsWith(findBy, StringComparison.OrdinalIgnoreCase)) + yield return map; + } + } + + public static Result GetById(int id) + { + if (id < 0 || id >= Count) + return Result.Failure(Messages.InvalidMap); + + Map map = s_maps[id]; + return Result.Success(map); + } + + public static Result GetByName(string mapName) + { + Map map = s_maps + .FirstOrDefault(map => map.Name.Equals(mapName, StringComparison.OrdinalIgnoreCase)); + return map is null ? + Result.Failure(Messages.MapNotFound) : + Result.Success(map); + } + + private class Map : IMap + { + public int Id { get; init; } + public string Name { get; init; } + } +} diff --git a/src/Application/Maps/Services/MapInfoService.cs b/src/Application/Maps/Services/MapInfoService.cs new file mode 100644 index 00000000..b2a5ca68 --- /dev/null +++ b/src/Application/Maps/Services/MapInfoService.cs @@ -0,0 +1,77 @@ +namespace CTF.Application.Maps.Services; + +/// +/// Represents a service to load information from a map. +/// +public class MapInfoService +{ + private CurrentMap _currentMap; + public MapInfoService() + { + int defaultMapId = 0; + IMap defaultMap = MapCollection.GetById(defaultMapId).Value; + Load(defaultMap); + } + + /// + /// Reads the current information from a map. + /// + public CurrentMap Read() => _currentMap; + + /// + /// Loads map information from the file system. + /// + /// The map to load. + /// + public void Load(IMap map) + { + ArgumentNullException.ThrowIfNull(map); + var basePath = AppContext.BaseDirectory; + var path = Path.Combine(basePath, "Maps", "Files", $"{map.Name}.ini"); + ISectionsData sections = SectionsFile.Load(path); + SpawnLocation[] alphaTeamLocations = GetLocations(sections["Alpha"]); + SpawnLocation[] betaTeamLocations = GetLocations(sections["Beta"]); + sections.TryGetData(section: "Interior", out ISectionData retrievedInterior); + sections.TryGetData(section: "Weather", out ISectionData retrievedWeather); + sections.TryGetData(section: "WorldTime", out ISectionData retrievedWorldTime); + + int interior = retrievedInterior is null ? + CurrentMap.DefaultInterior : + int.Parse(retrievedInterior.First()); + + int weather = retrievedWeather is null ? + CurrentMap.DefaultWeather : + int.Parse(retrievedWeather.First()); + + int worldTime = retrievedWorldTime is null ? + CurrentMap.DefaultWorldTime : + int.Parse(retrievedWorldTime.First()); + + _currentMap = new CurrentMap( + map, + alphaTeamLocations, + betaTeamLocations, + interior, + weather, + worldTime); + } + + private static SpawnLocation[] GetLocations(ISectionData section) + { + var locations = new SpawnLocation[section.Count]; + for(int i = 0; i < section.Count; i++) + { + string data = section[i]; + string[] coordinates = data.Split(','); + var position = new Vector3( + float.Parse(coordinates[0], CultureInfo.InvariantCulture), + float.Parse(coordinates[1], CultureInfo.InvariantCulture), + float.Parse(coordinates[2], CultureInfo.InvariantCulture) + ); + float angle = float.Parse(coordinates[3], CultureInfo.InvariantCulture); + var spawnLocation = new SpawnLocation(position, angle); + locations[i] = spawnLocation; + } + return locations; + } +} diff --git a/src/Application/Maps/SpawnLocation.cs b/src/Application/Maps/SpawnLocation.cs new file mode 100644 index 00000000..333b71dc --- /dev/null +++ b/src/Application/Maps/SpawnLocation.cs @@ -0,0 +1,19 @@ +namespace CTF.Application.Maps; + +public class SpawnLocation +{ + public static readonly SpawnLocation Empty = new(0, 0, 0, 0); + public Vector3 Position { get; } + public float Angle { get; } + public SpawnLocation(float x, float y, float z, float angle) + { + Position = new Vector3(x, y, z); + Angle = angle; + } + + public SpawnLocation(Vector3 position, float angle) + { + Position = position; + Angle = angle; + } +} diff --git a/src/Application/Maps/TimeLeft.cs b/src/Application/Maps/TimeLeft.cs new file mode 100644 index 00000000..d1a08eb2 --- /dev/null +++ b/src/Application/Maps/TimeLeft.cs @@ -0,0 +1,110 @@ +namespace CTF.Application.Maps; + +/// +/// Represents the time left on the current map. +/// +public class TimeLeft +{ + private const int MaxRoundTime = 900; + /// + /// Represents the interval in seconds. + /// + private int _interval = MaxRoundTime; + + /// + /// Represents the time left in a text draw. + /// + // This property can never be mutable. + // If this property is modified from the outside, it may cause buffer overflow. + public string TextDraw { get; } = "00:00"; + + public TimeLeft() => UpdateTextDraw(); + + /// + /// Checks if the countdown has ended. + /// + public bool IsCompleted() => _interval == 0; + + public Result SetInterval(Minutes minutes) + { + if (minutes.Value < 0 || minutes.Value > (MaxRoundTime / 60)) + { + var message = Smart.Format(Messages.InvalidInterval, new { Max = MaxRoundTime / 60 }); + return Result.Failure(message); + } + + _interval = minutes.Value * 60; + UpdateTextDraw(); + return Result.Success(); + } + + public Result SetInterval(Seconds seconds) + { + if (seconds.Value < 0 || seconds.Value > MaxRoundTime) + { + var message = Smart.Format(Messages.InvalidInterval, new { Max = MaxRoundTime }); + return Result.Failure(message); + } + + _interval = seconds.Value; + UpdateTextDraw(); + return Result.Success(); + } + + /// + /// Reduces the time remaining until it reaches zero. + /// + public void Decrease() + { + if(_interval == 0) + return; + + _interval--; + UpdateTextDraw(); + } + + public void Reset() + { + _interval = MaxRoundTime; + UpdateTextDraw(); + } + + /// + /// The purpose is to manipulate the buffer directly with pointers + /// to avoid memory reallocations caused by string interpolation. + /// + /// + /// This decision was made because the text will be updated every 1 ms by a timer. + /// + private unsafe void UpdateTextDraw() + { + int minutes = _interval / 60; + int seconds = _interval % 60; + + int digit1 = minutes % 10; + int digit0 = minutes / 10 % 10; + + int digit4 = seconds % 10; + int digit3 = seconds / 10 % 10; + + fixed (char* text = TextDraw) + { + text[0] = (char)(digit0 + '0'); + text[1] = (char)(digit1 + '0'); + text[3] = (char)(digit3 + '0'); + text[4] = (char)(digit4 + '0'); + } + } +} + +public readonly ref struct Minutes +{ + public int Value { get; } + public Minutes(int value) => Value = value; +} + +public readonly ref struct Seconds +{ + public int Value { get; } + public Seconds(int value) => Value = value; +} diff --git a/src/Application/Usings.cs b/src/Application/Usings.cs index 213af353..e155e2b8 100644 --- a/src/Application/Usings.cs +++ b/src/Application/Usings.cs @@ -1,4 +1,5 @@ global using System.Collections; +global using System.Globalization; global using System.Text.RegularExpressions; global using SampSharp.Core; global using SampSharp.Entities; @@ -6,6 +7,7 @@ global using Color = SampSharp.Entities.SAMP.Color; global using SampSharp.Entities.SAMP.Commands; global using SmartFormat; +global using SeztionParser; global using Microsoft.Extensions.DependencyInjection; global using CTF.Application.Common; diff --git a/tests/Application.Tests/Maps/CurrentMapTests.cs b/tests/Application.Tests/Maps/CurrentMapTests.cs new file mode 100644 index 00000000..132cdfb2 --- /dev/null +++ b/tests/Application.Tests/Maps/CurrentMapTests.cs @@ -0,0 +1,184 @@ +namespace CTF.Application.Tests.Maps; + +public class CurrentMapTests +{ + [Test] + public void Constructor_WhenMapIsNull_ShouldThrowArgumentNullException() + { + // Arrange + IMap map = default; + List locations = [SpawnLocation.Empty]; + + // Act + Action act = () => + { + var currentMap = new CurrentMap(map, locations, locations); + }; + + // Assert + act.Should() + .Throw() + .WithParameterName(nameof(map)); + } + + [Test] + public void Constructor_WhenAlphaTeamLocationsIsNull_ShouldThrowArgumentNullException() + { + // Arrange + IMap map = MapCollection.GetById(0).Value; + List alphaTeamLocations = default; + List betaTeamLocations = [SpawnLocation.Empty]; + + // Act + Action act = () => + { + var currentMap = new CurrentMap(map, alphaTeamLocations, betaTeamLocations); + }; + + // Assert + act.Should() + .Throw() + .WithParameterName(nameof(alphaTeamLocations)); + } + + [Test] + public void Constructor_WhenBetaTeamLocationsIsNull_ShouldThrowArgumentNullException() + { + // Arrange + IMap map = MapCollection.GetById(0).Value; + List betaTeamLocations = default; + List alphaTeamLocations = [SpawnLocation.Empty]; + + // Act + Action act = () => + { + var currentMap = new CurrentMap(map, alphaTeamLocations, betaTeamLocations); + }; + + // Assert + act.Should() + .Throw() + .WithParameterName(nameof(betaTeamLocations)); + } + + [Test] + public void Constructor_WhenAlphaTeamLocationsIsEmpty_ShouldThrowArgumentException() + { + // Arrange + IMap map = MapCollection.GetById(0).Value; + List alphaTeamLocations = []; + List betaTeamLocations = [SpawnLocation.Empty]; + + // Act + Action act = () => + { + var currentMap = new CurrentMap(map, alphaTeamLocations, betaTeamLocations); + }; + + // Assert + act.Should() + .Throw() + .WithParameterName(nameof(alphaTeamLocations)); + } + + [Test] + public void Constructor_WhenBetaTeamLocationsIsEmpty_ShouldThrowArgumentException() + { + // Arrange + IMap map = MapCollection.GetById(0).Value; + List alphaTeamLocations = [SpawnLocation.Empty]; + List betaTeamLocations = []; + + // Act + Action act = () => + { + var currentMap = new CurrentMap(map, alphaTeamLocations, betaTeamLocations); + }; + + // Assert + act.Should() + .Throw() + .WithParameterName(nameof(betaTeamLocations)); + } + + [TestCaseSource(typeof(GetNextMapTestCases))] + public void NextMap_WhenMapIsObtained_ShouldReturnsNextMap((IMap Map, IMap Expected) data) + { + // Arrange + List locations = [SpawnLocation.Empty]; + var currentMap = new CurrentMap(data.Map, locations, locations); + + // Act + IMap actual = currentMap.NextMap; + + // Assert + actual.Should().Be(data.Expected); + } + + [Test] + public void GetMapNameAsText_WhenNameIsObtained_ShouldReturnsValidStringFormat() + { + // Arrange + IMap map = MapCollection.GetByName("RC_Battlefield").Value; + List locations = [SpawnLocation.Empty]; + var currentMap = new CurrentMap(map, locations, locations); + var expectedString = "Map: ~w~RC_Battlefield"; + + // Act + string actual = currentMap.GetMapNameAsText(); + + // Assert + actual.Should().Be(expectedString); + } + + [Test] + public void SetNextMap_WhenArgumentIsNull_ShouldThrowArgumentNullException() + { + // Arrange + IMap map = MapCollection.GetById(0).Value; + IMap nextMap = default; + List locations = [SpawnLocation.Empty]; + var currentMap = new CurrentMap(map, locations, locations); + + // Act + Action act = () => currentMap.SetNextMap(nextMap); + + // Assert + act.Should() + .Throw() + .WithParameterName(nameof(nextMap)); + } + + [TestCase(TeamId.Alpha)] + [TestCase(TeamId.Beta)] + public void GetRandomSpawnLocation_WhenTeamIsAlphaOrBeta_ShouldReturnsSpawnLocation(TeamId team) + { + // Arrange + IMap map = MapCollection.GetById(0).Value; + List locations = [SpawnLocation.Empty]; + var currentMap = new CurrentMap(map, locations, locations); + SpawnLocation expectedSpawnLocation = locations[0]; + + // Act + SpawnLocation actual = currentMap.GetRandomSpawnLocation(team); + + // Assert + actual.Should().Be(expectedSpawnLocation); + } + + [Test] + public void GetRandomSpawnLocation_WhenTeamIsNotAlphaOrBeta_ShouldThrowNotSupportedException() + { + // Arrange + IMap map = MapCollection.GetById(0).Value; + List locations = [SpawnLocation.Empty]; + TeamId team = TeamId.NoTeam; + var currentMap = new CurrentMap(map, locations, locations); + + // Act + Action act = () => currentMap.GetRandomSpawnLocation(team); + + // Assert + act.Should().Throw(); + } +} diff --git a/tests/Application.Tests/Maps/DecreaseTimeLeftTestCases.cs b/tests/Application.Tests/Maps/DecreaseTimeLeftTestCases.cs new file mode 100644 index 00000000..85de1a7f --- /dev/null +++ b/tests/Application.Tests/Maps/DecreaseTimeLeftTestCases.cs @@ -0,0 +1,191 @@ +namespace CTF.Application.Tests.Maps; + +public class DecreaseTimeLeftTestCases : IEnumerable +{ + public IEnumerator GetEnumerator() + { + yield return "02:59"; + yield return "02:58"; + yield return "02:57"; + yield return "02:56"; + yield return "02:55"; + yield return "02:54"; + yield return "02:53"; + yield return "02:52"; + yield return "02:51"; + yield return "02:50"; + yield return "02:49"; + yield return "02:48"; + yield return "02:47"; + yield return "02:46"; + yield return "02:45"; + yield return "02:44"; + yield return "02:43"; + yield return "02:42"; + yield return "02:41"; + yield return "02:40"; + yield return "02:39"; + yield return "02:38"; + yield return "02:37"; + yield return "02:36"; + yield return "02:35"; + yield return "02:34"; + yield return "02:33"; + yield return "02:32"; + yield return "02:31"; + yield return "02:30"; + yield return "02:29"; + yield return "02:28"; + yield return "02:27"; + yield return "02:26"; + yield return "02:25"; + yield return "02:24"; + yield return "02:23"; + yield return "02:22"; + yield return "02:21"; + yield return "02:20"; + yield return "02:19"; + yield return "02:18"; + yield return "02:17"; + yield return "02:16"; + yield return "02:15"; + yield return "02:14"; + yield return "02:13"; + yield return "02:12"; + yield return "02:11"; + yield return "02:10"; + yield return "02:09"; + yield return "02:08"; + yield return "02:07"; + yield return "02:06"; + yield return "02:05"; + yield return "02:04"; + yield return "02:03"; + yield return "02:02"; + yield return "02:01"; + yield return "02:00"; + yield return "01:59"; + yield return "01:58"; + yield return "01:57"; + yield return "01:56"; + yield return "01:55"; + yield return "01:54"; + yield return "01:53"; + yield return "01:52"; + yield return "01:51"; + yield return "01:50"; + yield return "01:49"; + yield return "01:48"; + yield return "01:47"; + yield return "01:46"; + yield return "01:45"; + yield return "01:44"; + yield return "01:43"; + yield return "01:42"; + yield return "01:41"; + yield return "01:40"; + yield return "01:39"; + yield return "01:38"; + yield return "01:37"; + yield return "01:36"; + yield return "01:35"; + yield return "01:34"; + yield return "01:33"; + yield return "01:32"; + yield return "01:31"; + yield return "01:30"; + yield return "01:29"; + yield return "01:28"; + yield return "01:27"; + yield return "01:26"; + yield return "01:25"; + yield return "01:24"; + yield return "01:23"; + yield return "01:22"; + yield return "01:21"; + yield return "01:20"; + yield return "01:19"; + yield return "01:18"; + yield return "01:17"; + yield return "01:16"; + yield return "01:15"; + yield return "01:14"; + yield return "01:13"; + yield return "01:12"; + yield return "01:11"; + yield return "01:10"; + yield return "01:09"; + yield return "01:08"; + yield return "01:07"; + yield return "01:06"; + yield return "01:05"; + yield return "01:04"; + yield return "01:03"; + yield return "01:02"; + yield return "01:01"; + yield return "01:00"; + yield return "00:59"; + yield return "00:58"; + yield return "00:57"; + yield return "00:56"; + yield return "00:55"; + yield return "00:54"; + yield return "00:53"; + yield return "00:52"; + yield return "00:51"; + yield return "00:50"; + yield return "00:49"; + yield return "00:48"; + yield return "00:47"; + yield return "00:46"; + yield return "00:45"; + yield return "00:44"; + yield return "00:43"; + yield return "00:42"; + yield return "00:41"; + yield return "00:40"; + yield return "00:39"; + yield return "00:38"; + yield return "00:37"; + yield return "00:36"; + yield return "00:35"; + yield return "00:34"; + yield return "00:33"; + yield return "00:32"; + yield return "00:31"; + yield return "00:30"; + yield return "00:29"; + yield return "00:28"; + yield return "00:27"; + yield return "00:26"; + yield return "00:25"; + yield return "00:24"; + yield return "00:23"; + yield return "00:22"; + yield return "00:21"; + yield return "00:20"; + yield return "00:19"; + yield return "00:18"; + yield return "00:17"; + yield return "00:16"; + yield return "00:15"; + yield return "00:14"; + yield return "00:13"; + yield return "00:12"; + yield return "00:11"; + yield return "00:10"; + yield return "00:09"; + yield return "00:08"; + yield return "00:07"; + yield return "00:06"; + yield return "00:05"; + yield return "00:04"; + yield return "00:03"; + yield return "00:02"; + yield return "00:01"; + yield return "00:00"; + } + + IEnumerator IEnumerable.GetEnumerator() + => this.GetEnumerator(); +} diff --git a/tests/Application.Tests/Maps/GetNextMapTestCases.cs b/tests/Application.Tests/Maps/GetNextMapTestCases.cs new file mode 100644 index 00000000..e49ca391 --- /dev/null +++ b/tests/Application.Tests/Maps/GetNextMapTestCases.cs @@ -0,0 +1,26 @@ +namespace CTF.Application.Tests.Maps; + +public class GetNextMapTestCases : IEnumerable<(IMap Map, IMap Expected)> +{ + public IEnumerator<(IMap Map, IMap Expected)> GetEnumerator() + { + yield return (GetCurrentMap(0), GetNextMap(1)); + yield return (GetCurrentMap(1), GetNextMap(2)); + yield return (GetCurrentMap(2), GetNextMap(3)); + yield return (GetCurrentMap(3), GetNextMap(4)); + yield return (GetCurrentMap(4), GetNextMap(5)); + yield return (GetCurrentMap(5), GetNextMap(6)); + yield return (GetCurrentMap(6), GetNextMap(7)); + yield return (GetCurrentMap(7), GetNextMap(8)); + yield return (GetCurrentMap(31), GetNextMap(32)); + yield return ( + GetCurrentMap(MapCollection.Count - 1), + GetNextMap(0) + ); + } + + private static IMap GetCurrentMap(int id) => MapCollection.GetById(id).Value; + private static IMap GetNextMap(int id) => MapCollection.GetById(id).Value; + IEnumerator IEnumerable.GetEnumerator() + => this.GetEnumerator(); +} diff --git a/tests/Application.Tests/Maps/LoadTimeTests.cs b/tests/Application.Tests/Maps/LoadTimeTests.cs new file mode 100644 index 00000000..feaa017e --- /dev/null +++ b/tests/Application.Tests/Maps/LoadTimeTests.cs @@ -0,0 +1,112 @@ +namespace CTF.Application.Tests.Maps; + +public class LoadTimeTests +{ + static readonly int[] ExpectedIntervalCases = [5, 4, 3, 2, 1, 0]; + private readonly LoadTime _loadTime; + public LoadTimeTests() + { + static void OnLoadingMap() { } + static void OnLoadedMap() { } + _loadTime = new LoadTime(OnLoadingMap, OnLoadedMap); + } + + [Test] + public void Constructor_WhenOnLoadingMapIsNull_ShouldThrowArgumentNullException() + { + // Arrange + Action onLoadingMap = default; + static void OnLoadedMap() { } + + // Act + Action act = () => + { + var loadTime = new LoadTime(onLoadingMap, OnLoadedMap); + }; + + // Assert + act.Should() + .Throw() + .WithParameterName(nameof(onLoadingMap)); + } + + [Test] + public void Constructor_WhenOnLoadedMapIsNull_ShouldThrowArgumentNullException() + { + // Arrange + Action onLoadedMap = default; + static void OnLoadingMap() { } + + // Act + Action act = () => + { + var loadTime = new LoadTime(OnLoadingMap, onLoadedMap); + }; + + // Assert + act.Should() + .Throw() + .WithParameterName(nameof(onLoadedMap)); + } + + [TestCaseSource(nameof(ExpectedIntervalCases))] + public void Decrease_WhenLoadTimeIsNotCompleted_ShouldContinueToDecrease(int expectedInterval) + { + // Arrange + var expectedText = $"Loading map... ({expectedInterval})"; + + // Act + _loadTime.Decrease(); + int currentInterval = _loadTime.Interval; + + // Asserts + currentInterval.Should().Be(expectedInterval); + _loadTime.GameText.Should().Be(expectedText); + } + + [Test] + public void Decrease_WhenIntervalIsEqualsToZero_ShouldInvokeOnLoadedMap() + { + // Arrange + bool loadedMap = false; + static void OnLoadingMap() { } + void OnLoadedMap() => loadedMap = true; + var loadTime = new LoadTime(OnLoadingMap, OnLoadedMap); + int expectedInterval = LoadTime.MaxLoadTime; + loadTime.Decrease(); // 5 + loadTime.Decrease(); // 4 + loadTime.Decrease(); // 3 + loadTime.Decrease(); // 2 + loadTime.Decrease(); // 1 + loadTime.Decrease(); // 0 + + // Act + // Invoke to OnLoadedMap + loadTime.Decrease(); + + // Asserts + loadedMap.Should().BeTrue(); + loadTime.Interval.Should().Be(expectedInterval); + loadTime.GameText.Should().BeEmpty(); + } + + [Test] + public void Decrease_WhenIntervalIsEqualsToMaxLoadTime_ShouldInvokeOnLoadingMap() + { + // Arrange + bool loadingMap = false; + void OnLoadingMap() => loadingMap = true; + static void OnLoadedMap() { } + var loadTime = new LoadTime(OnLoadingMap, OnLoadedMap); + int expectedInterval = 5; + + // Act + // Invoke to OnLoadingMap + loadTime.Decrease(); + + // Asserts + loadingMap.Should().BeTrue(); + loadTime.Interval.Should().Be(expectedInterval); + loadTime.GameText.Should().NotBeEmpty(); + } +} diff --git a/tests/Application.Tests/Maps/MapCollectionTests.cs b/tests/Application.Tests/Maps/MapCollectionTests.cs new file mode 100644 index 00000000..dc1b4e25 --- /dev/null +++ b/tests/Application.Tests/Maps/MapCollectionTests.cs @@ -0,0 +1,96 @@ +namespace CTF.Application.Tests.Maps; + +public class MapCollectionTests +{ + static readonly int[] InvalidMapCases = [-1, 1000, MapCollection.Count]; + + [TestCase("de")] + [TestCase("DE")] + [TestCase("dE")] + [TestCase("De")] + public void GetAll_WhenAllMapsAreObtainedWithFindBy_ShouldReturnsEnumerable(string findBy) + { + // Arrange + string[] expectedMaps = + [ + "de_aztec", + "de_dust2", + "de_dust2_small", + "de_dust2_x1", + "de_dust2_x2", + "de_dust2_x3", + "de_dust5", + "DesertGlory" + ]; + + // Act + IEnumerable maps = MapCollection.GetAll(findBy); + string[] actual = maps.Select(map => map.Name).ToArray(); + + // Assert + actual.Should().BeEquivalentTo(expectedMaps); + } + + [TestCaseSource(nameof(InvalidMapCases))] + public void GetById_WhenMapIdIsInvalid_ShouldReturnsFailureResult(int mapId) + { + // Arrange + string expectedMessage = Messages.InvalidMap; + + // Act + Result result = MapCollection.GetById(mapId); + + // Asserts + result.IsSuccess.Should().BeFalse(); + result.Message.Should().Be(expectedMessage); + } + + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + [TestCase(4)] + public void GetById_WhenMapIdIsValid_ShouldReturnsSuccessResult(int mapId) + { + // Arrange + + // Act + Result result = MapCollection.GetById(mapId); + + // Asserts + result.IsSuccess.Should().BeTrue(); + result.Value.Id.Should().Be(mapId); + result.Value.Name.Should().NotBeNullOrEmpty(); + } + + [Test] + public void GetByName_WhenMapNameIsNotFound_ShouldReturnsFailureResult() + { + // Arrange + string mapName = "NotFound"; + string expectedMessage = Messages.MapNotFound; + + // Act + Result result = MapCollection.GetByName(mapName); + + // Asserts + result.IsSuccess.Should().BeFalse(); + result.Message.Should().Be(expectedMessage); + } + + [TestCase("de_aztec")] + [TestCase("DE_AZTEC")] + [TestCase("De_Aztec")] + public void GetByName_WhenMapNameIsFound_ShouldReturnsSuccessResult(string mapName) + { + // Arrange + string expectedMapName = "de_aztec"; + + // Act + Result result = MapCollection.GetByName(mapName); + + // Asserts + result.IsSuccess.Should().BeTrue(); + result.Value.Name.Should().Be(expectedMapName); + } +} diff --git a/tests/Application.Tests/Maps/MapInfoServiceTests.cs b/tests/Application.Tests/Maps/MapInfoServiceTests.cs new file mode 100644 index 00000000..a0f5bcab --- /dev/null +++ b/tests/Application.Tests/Maps/MapInfoServiceTests.cs @@ -0,0 +1,163 @@ +namespace CTF.Application.Tests.Maps; + +public class MapInfoServiceTests +{ + [Test] + public void Constructor_WhenLoadMethodIsNotInvoked_CurrentMapShouldNotBeNull() + { + // Arrange + + // Act + var service = new MapInfoService(); + CurrentMap currentMap = service.Read(); + + // Assert + currentMap.Should().NotBeNull(); + } + + [TestCaseSource(typeof(MapInfoServiceTestCases))] + public void Load_WhenMapIsLoadedFromFileSystem_ShouldCreateInstanceOfTypeCurrentMap(CurrentMap expectedCurrentMap) + { + // Arrange + var service = new MapInfoService(); + IMap map = MapCollection.GetById(expectedCurrentMap.Id).Value; + + // Act + service.Load(map); + CurrentMap actual = service.Read(); + + // Assert + actual.Should().BeEquivalentTo(expectedCurrentMap); + } + + private class MapInfoServiceTestCases : IEnumerable + { + public IEnumerator GetEnumerator() + { + yield return new CurrentMap( + map: GetMap("Aim_Headshot"), + alphaTeamLocations: + [ + new SpawnLocation(-129.5612f,81.0056f,3.1172f,156.7189f), + new SpawnLocation(-127.6526f,87.7695f,3.1172f,156.7189f), + new SpawnLocation(-134.9525f,90.2646f,3.1172f,167.0590f), + new SpawnLocation(-138.4023f,83.5352f,3.1172f,163.6123f), + new SpawnLocation(-145.3216f,85.1476f,3.1172f,163.6123f), + new SpawnLocation(-152.4534f,80.4830f,3.1094f,163.6123f), + new SpawnLocation(-161.3326f,83.2152f,3.1094f,167.3724f), + new SpawnLocation(-159.6665f,98.1220f,3.1121f,165.8291f), + new SpawnLocation(-173.8980f,102.8791f,3.1668f,162.3824f), + new SpawnLocation(-186.4793f,93.0401f,3.1172f,162.3824f) + ], + betaTeamLocations: + [ + new SpawnLocation(-277.0338f,-85.0175f,2.8617f,345.0341f), + new SpawnLocation(-277.7510f,-90.4126f,2.7030f,345.0341f), + new SpawnLocation(-270.0297f,-92.0674f,3.0969f,345.0341f), + new SpawnLocation(-263.8904f,-93.2464f,3.1172f,345.0341f), + new SpawnLocation(-262.2849f,-87.2403f,3.1172f,345.0341f), + new SpawnLocation(-255.3565f,-84.1217f,3.1172f,345.0341f), + new SpawnLocation(-245.9794f,-86.1564f,3.1172f,345.0341f), + new SpawnLocation(-247.4499f,-99.9677f,3.1172f,345.0341f), + new SpawnLocation(-235.9863f,-102.5671f,3.1094f,345.3474f), + new SpawnLocation(-220.4884f,-110.9102f,3.1172f,352.5542f) + ]); + + yield return new CurrentMap( + map: GetMap("RC_Battlefield"), + alphaTeamLocations: + [ + new SpawnLocation(-1136.4539f,1098.1666f,1345.8258f,269.4457f), + new SpawnLocation(-1135.1912f,1093.8064f,1345.8119f,267.8791f), + new SpawnLocation(-1135.6179f,1088.5378f,1345.8096f,267.8791f), + new SpawnLocation(-1135.4375f,1082.0457f,1345.8021f,267.8791f), + new SpawnLocation(-1135.2130f,1075.4181f,1345.7941f,267.8791f), + new SpawnLocation(-1135.0983f,1066.7756f,1345.7872f,267.8791f), + new SpawnLocation(-1129.9963f,1067.1566f,1345.7528f,274.1458f), + new SpawnLocation(-1129.5995f,1074.0201f,1345.7581f,274.1458f), + new SpawnLocation(-1129.0844f,1081.9758f,1345.7615f,268.5057f), + new SpawnLocation(-1131.0197f,1092.0566f,1345.7826f,264.1190f) + ], + betaTeamLocations: + [ + new SpawnLocation(-969.8113f,1021.9630f,1345.0767f,89.9275f), + new SpawnLocation(-970.1436f,1028.6224f,1345.0679f,88.9874f), + new SpawnLocation(-970.4197f,1036.5406f,1345.0582f,88.9874f), + new SpawnLocation(-969.6072f,1046.1462f,1345.0544f,88.9874f), + new SpawnLocation(-970.4572f,1055.3048f,1345.0397f,88.9874f), + new SpawnLocation(-976.7345f,1054.0677f,1344.9979f,90.8674f), + new SpawnLocation(-976.9385f,1044.5823f,1345.0059f,88.6741f), + new SpawnLocation(-977.0988f,1035.4301f,1345.0137f,88.6741f), + new SpawnLocation(-976.4217f,1024.6754f,1345.0288f,92.1208f), + new SpawnLocation(-976.2883f,1020.3914f,1345.0339f,93.3741f) + ], + interior: 10); + + yield return new CurrentMap( + map: GetMap("TheBunker"), + alphaTeamLocations: + [ + new SpawnLocation(592.8154f,-2433.9148f,10.8944f,79.0350f), + new SpawnLocation(593.3266f,-2430.6772f,10.8968f,75.2750f), + new SpawnLocation(593.7708f,-2427.1843f,10.9065f,81.8550f), + new SpawnLocation(594.2177f,-2425.0789f,10.9081f,78.0950f), + new SpawnLocation(591.5002f,-2424.4595f,10.9011f,77.1550f), + new SpawnLocation(590.2183f,-2427.9417f,10.8983f,77.1550f), + new SpawnLocation(588.6526f,-2431.8225f,10.8952f,77.1550f), + new SpawnLocation(584.6981f,-2430.9185f,10.9023f,348.4808f), + new SpawnLocation(575.6008f,-2417.5117f,10.9036f,252.9133f), + new SpawnLocation(584.4872f,-2417.1072f,10.9053f,166.1425f) + ], + betaTeamLocations: + [ + new SpawnLocation(891.1278f,-2397.3025f,19.8204f,87.1585f), + new SpawnLocation(889.6392f,-2407.8728f,20.2593f,80.5784f), + new SpawnLocation(887.4858f,-2418.1658f,21.0038f,80.5784f), + new SpawnLocation(885.3065f,-2425.9482f,21.5207f,49.8714f), + new SpawnLocation(875.5358f,-2419.4922f,21.7868f,67.4183f), + new SpawnLocation(876.6066f,-2412.1060f,22.0539f,80.2651f), + new SpawnLocation(877.5095f,-2404.7627f,22.3557f,80.2651f), + new SpawnLocation(878.8505f,-2396.0938f,22.6503f,92.4852f), + new SpawnLocation(870.1289f,-2393.8157f,23.9384f,92.4852f), + new SpawnLocation(864.5809f,-2400.9045f,25.0956f,94.6786f) + ], + worldTime: 23); + + yield return new CurrentMap( + map: GetMap("CrackFactory"), + alphaTeamLocations: + [ + new SpawnLocation(2548.7009f,-1283.2224f,1060.9844f,230.3022f), + new SpawnLocation(2565.8301f,-1281.7773f,1065.3672f,238.1356f), + new SpawnLocation(2575.7759f,-1283.3206f,1065.3672f,177.9750f), + new SpawnLocation(2580.8525f,-1284.6443f,1065.3579f,88.0476f), + new SpawnLocation(2568.5518f,-1283.6564f,1060.9844f,181.0851f), + new SpawnLocation(2565.0220f,-1290.9614f,1060.9844f,224.6389f), + new SpawnLocation(2565.4963f,-1301.7297f,1060.9844f,275.0860f), + new SpawnLocation(2558.1384f,-1304.1283f,1060.9844f,272.2661f), + new SpawnLocation(2558.3372f,-1298.2233f,1060.9844f,272.2661f), + new SpawnLocation(2558.2466f,-1293.2781f,1062.0391f,260.6727f) + ], + betaTeamLocations: + [ + new SpawnLocation(2532.1660f,-1283.6971f,1031.4219f,270.0725f), + new SpawnLocation(2532.5823f,-1292.2178f,1031.4219f,275.7126f), + new SpawnLocation(2532.9485f,-1302.3477f,1031.4219f,269.4458f), + new SpawnLocation(2541.2852f,-1303.9135f,1031.4219f,269.4458f), + new SpawnLocation(2542.1389f,-1293.7726f,1031.4219f,262.8658f), + new SpawnLocation(2542.5986f,-1282.2380f,1031.4219f,264.1191f), + new SpawnLocation(2550.1204f,-1282.6653f,1031.4219f,263.8058f), + new SpawnLocation(2551.0386f,-1293.5071f,1031.4219f,277.2793f), + new SpawnLocation(2553.0278f,-1305.5601f,1031.4219f,277.2793f), + new SpawnLocation(2560.5957f,-1291.2533f,1031.4219f,265.3724f) + ], + interior: 2, + weather: 9, + worldTime: 22); + } + + private static IMap GetMap(string name) => MapCollection.GetByName(name).Value; + IEnumerator IEnumerable.GetEnumerator() + => this.GetEnumerator(); + } +} diff --git a/tests/Application.Tests/Maps/TimeLeftTests.cs b/tests/Application.Tests/Maps/TimeLeftTests.cs new file mode 100644 index 00000000..c846dcb4 --- /dev/null +++ b/tests/Application.Tests/Maps/TimeLeftTests.cs @@ -0,0 +1,194 @@ +namespace CTF.Application.Tests.Maps; + +public class TimeLeftTests +{ + private readonly TimeLeft _timeLeft; + public TimeLeftTests() + { + _timeLeft = new TimeLeft(); + var minutes = new Minutes(3); + _timeLeft.SetInterval(minutes); + } + + [Test] + public void IsCompleted_WhenTimeLeftIsCompleted_ShouldReturnsTrue() + { + // Arrange + var timeLeft = new TimeLeft(); + var seconds = new Seconds(0); + var expectedText = "00:00"; + timeLeft.SetInterval(seconds); + + // Act + bool actual = timeLeft.IsCompleted(); + + // Asserts + actual.Should().BeTrue(); + timeLeft.TextDraw.Should().Be(expectedText); + } + + [Test] + public void IsCompleted_WhenTimeLeftIsNotCompleted_ShouldReturnsFalse() + { + // Arrange + var timeLeft = new TimeLeft(); + var minutes = new Minutes(10); + var expectedText = "10:00"; + timeLeft.SetInterval(minutes); + + // Act + bool actual = timeLeft.IsCompleted(); + + // Asserts + actual.Should().BeFalse(); + timeLeft.TextDraw.Should().Be(expectedText); + } + + [TestCase(-1)] + [TestCase(-2)] + [TestCase(16)] + [TestCase(17)] + public void SetInterval_WhenMinutesIntervalIsOutOfRange_ShouldReturnsFailureResult(int value) + { + // Arrange + var timeLeft = new TimeLeft(); + var minutes = new Minutes(value); + var expectedText = "15:00"; + + // Act + Result result = timeLeft.SetInterval(minutes); + + // Asserts + result.IsSuccess.Should().BeFalse(); + timeLeft.TextDraw.Should().Be(expectedText); + } + + [TestCase(0, "00:00")] + [TestCase(1, "01:00")] + [TestCase(3, "03:00")] + [TestCase(5, "05:00")] + [TestCase(9, "09:00")] + [TestCase(10, "10:00")] + [TestCase(11, "11:00")] + [TestCase(12, "12:00")] + [TestCase(14, "14:00")] + [TestCase(15, "15:00")] + public void SetInterval_WhenMinutesIntervalIsNotOutOfRange_ShouldReturnsSuccessResult(int value, string expectedText) + { + // Arrange + var timeLeft = new TimeLeft(); + var minutes = new Minutes(value); + + // Act + Result result = timeLeft.SetInterval(minutes); + + // Asserts + result.IsSuccess.Should().BeTrue(); + timeLeft.TextDraw.Should().Be(expectedText); + } + + [TestCase(-1)] + [TestCase(-2)] + [TestCase(901)] + [TestCase(902)] + public void SetInterval_WhenSecondsIntervalIsOutOfRange_ShouldReturnsFailureResult(int value) + { + // Arrange + var timeLeft = new TimeLeft(); + var seconds = new Seconds(value); + var expectedText = "15:00"; + + // Act + Result result = timeLeft.SetInterval(seconds); + + // Asserts + result.IsSuccess.Should().BeFalse(); + timeLeft.TextDraw.Should().Be(expectedText); + } + + [TestCase(0, "00:00")] + [TestCase(1, "00:01")] + [TestCase(5, "00:05")] + [TestCase(60, "01:00")] + [TestCase(300, "05:00")] + [TestCase(428, "07:08")] + [TestCase(590, "09:50")] + [TestCase(608, "10:08")] + [TestCase(840, "14:00")] + [TestCase(900, "15:00")] + public void SetInterval_WhenSecondsIntervalIsNotOutOfRange_ShouldReturnsSuccessResult(int value, string expectedText) + { + // Arrange + var timeLeft = new TimeLeft(); + var seconds = new Seconds(value); + + // Act + Result result = timeLeft.SetInterval(seconds); + + // Asserts + result.IsSuccess.Should().BeTrue(); + timeLeft.TextDraw.Should().Be(expectedText); + } + + [Test] + public void Constructor_WhenObjectIsCreated_TextDrawShouldBeTheDefault() + { + // Arrange + var expectedText = "15:00"; + + // Act + var timeLeft = new TimeLeft(); + + // Assert + timeLeft.TextDraw.Should().Be(expectedText); + } + + [Test] + public void Reset() + { + // Arrange + var timeLeft = new TimeLeft(); + var minutes = new Minutes(5); + var expectedText = "15:00"; + timeLeft.SetInterval(minutes); + + // Act + timeLeft.Reset(); + + // Assert + timeLeft.TextDraw.Should().Be(expectedText); + } + + [TestCaseSource(typeof(DecreaseTimeLeftTestCases))] + public void Decrease_WhenTimeRemainingIsNotCompleted_ShouldContinueToDecrease(string expectedText) + { + // Arrange + + // Act + _timeLeft.Decrease(); + + // Assert + _timeLeft.TextDraw.Should().Be(expectedText); + } + + [Test] + public void Decrease_WhenTimeRemainingIsZero_ShouldNotContinueToDecrease() + { + // Arrange + var timeLeft = new TimeLeft(); + var minutes = new Seconds(4); + var expectedText = "00:00"; + timeLeft.SetInterval(minutes); + + // Act + timeLeft.Decrease(); // 00:03 + timeLeft.Decrease(); // 00:02 + timeLeft.Decrease(); // 00:01 + timeLeft.Decrease(); // 00:00 + timeLeft.Decrease(); // 00:00 + timeLeft.Decrease(); // 00:00 + + // Assert + timeLeft.TextDraw.Should().Be(expectedText); + } +} diff --git a/tests/Application.Tests/Usings.cs b/tests/Application.Tests/Usings.cs index e0eb58bb..b2198178 100644 --- a/tests/Application.Tests/Usings.cs +++ b/tests/Application.Tests/Usings.cs @@ -11,3 +11,5 @@ global using CTF.Application.Players.Accounts; global using CTF.Application.Teams; global using CTF.Application.Teams.Flags; +global using CTF.Application.Maps; +global using CTF.Application.Maps.Services;