From 9ccb7c8df845ed8c86006c25942809201b1d86df Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Mon, 29 Apr 2024 16:39:37 +0200 Subject: [PATCH 1/5] add magazine system (cannot reload currently) --- tank-frontend/src/PlayerInfo.tsx | 4 +- .../TanksServer/GameLogic/CollectPowerUp.cs | 6 ++- .../TanksServer/GameLogic/ShootFromTanks.cs | 11 +++-- .../Interactivity/PlayerInfoConnection.cs | 23 ++-------- tanks-backend/TanksServer/Models/Magazine.cs | 44 +++++++++++++++++++ .../TanksServer/Models/PlayerControls.cs | 22 +++++++++- .../TanksServer/Models/PlayerInfo.cs | 6 +-- tanks-backend/TanksServer/Models/Tank.cs | 2 +- 8 files changed, 86 insertions(+), 32 deletions(-) create mode 100644 tanks-backend/TanksServer/Models/Magazine.cs diff --git a/tank-frontend/src/PlayerInfo.tsx b/tank-frontend/src/PlayerInfo.tsx index e6f9b16..8a3cfad 100644 --- a/tank-frontend/src/PlayerInfo.tsx +++ b/tank-frontend/src/PlayerInfo.tsx @@ -22,7 +22,7 @@ function ScoreRow({name, value}: { } type TankInfo = { - readonly explosiveBullets: number; + readonly magazine: string; readonly position: { x: number; y: number }; readonly orientation: number; readonly moving: boolean; @@ -63,8 +63,8 @@ export default function PlayerInfo({player}: { player: string }) { + - diff --git a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs index cc74e23..e0c5ec4 100644 --- a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs @@ -21,7 +21,11 @@ internal sealed class CollectPowerUp( continue; // now the tank overlaps the power up by at least 0.5 tiles - tank.ExplosiveBullets += 10; + tank.Magazine = tank.Magazine with + { + UsedBullets = 0, + Type = MagazineType.Explosive + }; tank.Owner.Scores.PowerUpsCollected++; return true; } diff --git a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs index e6bb65e..71637d6 100644 --- a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs +++ b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs @@ -21,15 +21,18 @@ internal sealed class ShootFromTanks( { if (!tank.Owner.Controls.Shoot) return; + if (tank.Magazine.Empty) + return; if (tank.NextShotAfter >= DateTime.Now) return; tank.NextShotAfter = DateTime.Now.AddMilliseconds(_config.ShootDelayMs); + tank.Magazine = tank.Magazine with + { + UsedBullets = (byte)(tank.Magazine.UsedBullets + 1) + }; - var explosive = tank.ExplosiveBullets > 0; - if (explosive) - tank.ExplosiveBullets--; - + var explosive = tank.Magazine.Type.HasFlag(MagazineType.Explosive); tank.Owner.Scores.ShotsFired++; entityManager.SpawnBullet(tank.Owner, tank.Position, tank.Orientation / 16d, explosive); } diff --git a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs index c8fbbe7..b231ec5 100644 --- a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs @@ -46,10 +46,10 @@ internal sealed class PlayerInfoConnection( private byte[]? GetMessageToSend() { var tank = entityManager.GetCurrentTankOfPlayer(player); - var tankInfo = tank != null - ? new TankInfo(tank.Orientation, tank.ExplosiveBullets, tank.Position.ToPixelPosition(), tank.Moving) + TankInfo? tankInfo = tank != null + ? new TankInfo(tank.Orientation, tank.Magazine.ToDisplayString(), tank.Position.ToPixelPosition(), tank.Moving) : null; - var info = new PlayerInfo(player.Name, player.Scores, ControlsToString(player.Controls), tankInfo); + var info = new PlayerInfo(player.Name, player.Scores, player.Controls.ToDisplayString(), tankInfo); var response = JsonSerializer.SerializeToUtf8Bytes(info, _context.PlayerInfo); if (response.SequenceEqual(_lastMessage)) @@ -57,21 +57,4 @@ internal sealed class PlayerInfoConnection( return _lastMessage = response; } - - private static string ControlsToString(PlayerControls controls) - { - var str = new StringBuilder("[ "); - if (controls.Forward) - str.Append("▲ "); - if (controls.Backward) - str.Append("▼ "); - if (controls.TurnLeft) - str.Append("◄ "); - if (controls.TurnRight) - str.Append("► "); - if (controls.Shoot) - str.Append("• "); - str.Append(']'); - return str.ToString(); - } } diff --git a/tanks-backend/TanksServer/Models/Magazine.cs b/tanks-backend/TanksServer/Models/Magazine.cs new file mode 100644 index 0000000..8980b80 --- /dev/null +++ b/tanks-backend/TanksServer/Models/Magazine.cs @@ -0,0 +1,44 @@ +using System.Text; + +namespace TanksServer.Models; + +[Flags] +internal enum MagazineType +{ + Basic = 1 << 0, + Fast = 1 << 1, + Explosive = 1 << 2, + Smart = 1 << 3, + Mine = 1 << 4, +} + +internal readonly record struct Magazine(MagazineType Type, byte UsedBullets, byte MaxBullets) +{ + public bool Empty => UsedBullets >= MaxBullets; + + public string ToDisplayString() + { + var sb = new StringBuilder(); + + if (Type.HasFlag(MagazineType.Fast)) + sb.Append('»'); + if (Type.HasFlag(MagazineType.Explosive)) + sb.Append('*'); + if (Type.HasFlag(MagazineType.Smart)) + sb.Append('@'); + if (Type.HasFlag(MagazineType.Mine)) + sb.Append('\u263c'); + + if (sb.Length > 0) + sb.Append(' '); + + sb.Append("[ "); + for (var i = 0; i < UsedBullets; i++) + sb.Append("\u25cb "); + for (var i = UsedBullets; i < MaxBullets; i++) + sb.Append("• "); + sb.Append(']'); + + return sb.ToString(); + } +} diff --git a/tanks-backend/TanksServer/Models/PlayerControls.cs b/tanks-backend/TanksServer/Models/PlayerControls.cs index 82e0bef..b5b82dd 100644 --- a/tanks-backend/TanksServer/Models/PlayerControls.cs +++ b/tanks-backend/TanksServer/Models/PlayerControls.cs @@ -1,3 +1,5 @@ +using System.Text; + namespace TanksServer.Models; internal sealed class PlayerControls @@ -7,4 +9,22 @@ internal sealed class PlayerControls public bool TurnLeft { get; set; } public bool TurnRight { get; set; } public bool Shoot { get; set; } -} \ No newline at end of file + + + public string ToDisplayString() + { + var str = new StringBuilder("[ "); + if (Forward) + str.Append("▲ "); + if (Backward) + str.Append("▼ "); + if (TurnLeft) + str.Append("◄ "); + if (TurnRight) + str.Append("► "); + if (Shoot) + str.Append("• "); + str.Append(']'); + return str.ToString(); + } +} diff --git a/tanks-backend/TanksServer/Models/PlayerInfo.cs b/tanks-backend/TanksServer/Models/PlayerInfo.cs index be46707..118eb42 100644 --- a/tanks-backend/TanksServer/Models/PlayerInfo.cs +++ b/tanks-backend/TanksServer/Models/PlayerInfo.cs @@ -1,13 +1,13 @@ namespace TanksServer.Models; -internal sealed record class TankInfo( +internal record struct TankInfo( int Orientation, - byte ExplosiveBullets, + string Magazine, PixelPosition Position, bool Moving ); -internal sealed record class PlayerInfo( +internal record struct PlayerInfo( string Name, Scores Scores, string Controls, diff --git a/tanks-backend/TanksServer/Models/Tank.cs b/tanks-backend/TanksServer/Models/Tank.cs index 5f64a81..3a1be87 100644 --- a/tanks-backend/TanksServer/Models/Tank.cs +++ b/tanks-backend/TanksServer/Models/Tank.cs @@ -30,5 +30,5 @@ internal sealed class Tank(Player player, FloatPosition spawnPosition) : IMapEnt public int Orientation => (int)Math.Round(Rotation * 16) % 16; - public byte ExplosiveBullets { get; set; } + public Magazine Magazine { get; set; } = new(MagazineType.Basic, 0, 5); } From 9164d904435fe92e8742d44061d1d2a3ba3af127 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Mon, 29 Apr 2024 16:59:37 +0200 Subject: [PATCH 2/5] implement fast bullets --- .../TanksServer/GameLogic/GameRules.cs | 2 ++ .../TanksServer/GameLogic/MapEntityManager.cs | 18 +++++++++++++----- .../TanksServer/GameLogic/MoveBullets.cs | 9 +++------ .../TanksServer/GameLogic/ShootFromTanks.cs | 5 +---- tanks-backend/TanksServer/Models/Bullet.cs | 2 ++ tanks-backend/TanksServer/Models/Magazine.cs | 10 +++++----- tanks-backend/TanksServer/Models/Tank.cs | 8 ++++---- 7 files changed, 30 insertions(+), 24 deletions(-) diff --git a/tanks-backend/TanksServer/GameLogic/GameRules.cs b/tanks-backend/TanksServer/GameLogic/GameRules.cs index 50d79e1..eb28672 100644 --- a/tanks-backend/TanksServer/GameLogic/GameRules.cs +++ b/tanks-backend/TanksServer/GameLogic/GameRules.cs @@ -21,4 +21,6 @@ internal sealed class GameRules public int SpawnDelayMs { get; set; } public int IdleTimeoutMs { get; set; } + + public byte MagazineSize { get; set; } = 5; } diff --git a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs index ce63a05..cd0ed6b 100644 --- a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs +++ b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs @@ -6,6 +6,7 @@ internal sealed class MapEntityManager( IOptions options ) { + private readonly GameRules _rules = options.Value; private readonly HashSet _bullets = []; private readonly HashSet _powerUps = []; private readonly Dictionary _playerTanks = []; @@ -15,24 +16,31 @@ internal sealed class MapEntityManager( public IEnumerable Tanks => _playerTanks.Values; public IEnumerable PowerUps => _powerUps; - public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, bool isExplosive) - => _bullets.Add(new Bullet + public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, MagazineType type) + { + var speed = _rules.BulletSpeed * (type.HasFlag(MagazineType.Fast) ? 2 : 1); + _bullets.Add(new Bullet { Owner = tankOwner, Position = position, Rotation = rotation, - IsExplosive = isExplosive, + IsExplosive = type.HasFlag(MagazineType.Explosive), Timeout = DateTime.Now + _bulletTimeout, OwnerCollisionAfter = DateTime.Now + TimeSpan.FromSeconds(1), + Speed = speed }); + } public void RemoveWhere(Predicate predicate) => _bullets.RemoveWhere(predicate); public void SpawnTank(Player player) { - var tank = new Tank(player, ChooseSpawnPosition()) + var tank = new Tank { - Rotation = Random.Shared.NextDouble() + Owner = player, + Position = ChooseSpawnPosition(), + Rotation = Random.Shared.NextDouble(), + Magazine = new Magazine(MagazineType.Basic, 0, _rules.MagazineSize) }; _playerTanks[player] = tank; logger.LogInformation("Tank added for player {}", player.Name); diff --git a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs index 63e3661..755e454 100644 --- a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs +++ b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs @@ -1,9 +1,6 @@ namespace TanksServer.GameLogic; -internal sealed class MoveBullets( - MapEntityManager entityManager, - IOptions config -) : ITickStep +internal sealed class MoveBullets(MapEntityManager entityManager) : ITickStep { public ValueTask TickAsync(TimeSpan delta) { @@ -13,9 +10,9 @@ internal sealed class MoveBullets( return ValueTask.CompletedTask; } - private void MoveBullet(Bullet bullet, TimeSpan delta) + private static void MoveBullet(Bullet bullet, TimeSpan delta) { - var speed = config.Value.BulletSpeed * delta.TotalSeconds; + var speed = bullet.Speed * delta.TotalSeconds; var angle = bullet.Rotation * 2 * Math.PI; bullet.Position = new FloatPosition( bullet.Position.X + Math.Sin(angle) * speed, diff --git a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs index 71637d6..86a46d3 100644 --- a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs +++ b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; - namespace TanksServer.GameLogic; internal sealed class ShootFromTanks( @@ -32,8 +30,7 @@ internal sealed class ShootFromTanks( UsedBullets = (byte)(tank.Magazine.UsedBullets + 1) }; - var explosive = tank.Magazine.Type.HasFlag(MagazineType.Explosive); tank.Owner.Scores.ShotsFired++; - entityManager.SpawnBullet(tank.Owner, tank.Position, tank.Orientation / 16d, explosive); + entityManager.SpawnBullet(tank.Owner, tank.Position, tank.Orientation / 16d, tank.Magazine.Type); } } diff --git a/tanks-backend/TanksServer/Models/Bullet.cs b/tanks-backend/TanksServer/Models/Bullet.cs index c6e87d9..f685741 100644 --- a/tanks-backend/TanksServer/Models/Bullet.cs +++ b/tanks-backend/TanksServer/Models/Bullet.cs @@ -15,4 +15,6 @@ internal sealed class Bullet : IMapEntity public PixelBounds Bounds => new(Position.ToPixelPosition(), Position.ToPixelPosition()); internal required DateTime OwnerCollisionAfter { get; init; } + + public required double Speed { get; init; } } diff --git a/tanks-backend/TanksServer/Models/Magazine.cs b/tanks-backend/TanksServer/Models/Magazine.cs index 8980b80..8994e61 100644 --- a/tanks-backend/TanksServer/Models/Magazine.cs +++ b/tanks-backend/TanksServer/Models/Magazine.cs @@ -5,11 +5,11 @@ namespace TanksServer.Models; [Flags] internal enum MagazineType { - Basic = 1 << 0, - Fast = 1 << 1, - Explosive = 1 << 2, - Smart = 1 << 3, - Mine = 1 << 4, + Basic = 0, + Fast = 1 << 0, + Explosive = 1 << 1, + Smart = 1 << 2, + Mine = 1 << 3, } internal readonly record struct Magazine(MagazineType Type, byte UsedBullets, byte MaxBullets) diff --git a/tanks-backend/TanksServer/Models/Tank.cs b/tanks-backend/TanksServer/Models/Tank.cs index 3a1be87..4d449a2 100644 --- a/tanks-backend/TanksServer/Models/Tank.cs +++ b/tanks-backend/TanksServer/Models/Tank.cs @@ -3,11 +3,11 @@ using TanksServer.GameLogic; namespace TanksServer.Models; -internal sealed class Tank(Player player, FloatPosition spawnPosition) : IMapEntity +internal sealed class Tank : IMapEntity { private double _rotation; - public Player Owner { get; } = player; + public required Player Owner { get; init; } public double Rotation { @@ -24,11 +24,11 @@ internal sealed class Tank(Player player, FloatPosition spawnPosition) : IMapEnt public bool Moving { get; set; } - public FloatPosition Position { get; set; } = spawnPosition; + public required FloatPosition Position { get; set; } public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize); public int Orientation => (int)Math.Round(Rotation * 16) % 16; - public Magazine Magazine { get; set; } = new(MagazineType.Basic, 0, 5); + public required Magazine Magazine { get; set; } } From a5a3ca30135071345df163195e766dbc424fff6b Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Mon, 29 Apr 2024 17:17:44 +0200 Subject: [PATCH 3/5] implement reloading --- .../TanksServer/GameLogic/CollectPowerUp.cs | 7 ++++++- .../TanksServer/GameLogic/GameRules.cs | 2 ++ .../TanksServer/GameLogic/ShootFromTanks.cs | 19 ++++++++++++++++--- .../Interactivity/PlayerInfoConnection.cs | 12 ++++++++---- tanks-backend/TanksServer/Models/Tank.cs | 2 ++ 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs index e0c5ec4..0195465 100644 --- a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs @@ -21,11 +21,16 @@ internal sealed class CollectPowerUp( continue; // now the tank overlaps the power up by at least 0.5 tiles + tank.Magazine = tank.Magazine with { UsedBullets = 0, - Type = MagazineType.Explosive + Type = tank.Magazine.Type | MagazineType.Explosive }; + + if (tank.ReloadingUntil >= DateTime.Now) + tank.ReloadingUntil = DateTime.Now; + tank.Owner.Scores.PowerUpsCollected++; return true; } diff --git a/tanks-backend/TanksServer/GameLogic/GameRules.cs b/tanks-backend/TanksServer/GameLogic/GameRules.cs index eb28672..5c4a98f 100644 --- a/tanks-backend/TanksServer/GameLogic/GameRules.cs +++ b/tanks-backend/TanksServer/GameLogic/GameRules.cs @@ -23,4 +23,6 @@ internal sealed class GameRules public int IdleTimeoutMs { get; set; } public byte MagazineSize { get; set; } = 5; + + public int ReloadDelayMs { get; set; } = 3000; } diff --git a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs index 86a46d3..9df8a64 100644 --- a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs +++ b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs @@ -19,12 +19,25 @@ internal sealed class ShootFromTanks( { if (!tank.Owner.Controls.Shoot) return; - if (tank.Magazine.Empty) + + var now = DateTime.Now; + if (tank.NextShotAfter >= now) return; - if (tank.NextShotAfter >= DateTime.Now) + if (tank.ReloadingUntil >= now) return; - tank.NextShotAfter = DateTime.Now.AddMilliseconds(_config.ShootDelayMs); + if (tank.Magazine.Empty) + { + tank.ReloadingUntil = now.AddMilliseconds(_config.ReloadDelayMs); + tank.Magazine = tank.Magazine with + { + UsedBullets = 0, + Type = MagazineType.Basic + }; + return; + } + + tank.NextShotAfter = now.AddMilliseconds(_config.ShootDelayMs); tank.Magazine = tank.Magazine with { UsedBullets = (byte)(tank.Magazine.UsedBullets + 1) diff --git a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs index b231ec5..bc4956b 100644 --- a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs @@ -1,5 +1,4 @@ using System.Net.WebSockets; -using System.Text; using System.Text.Json; using TanksServer.GameLogic; @@ -46,9 +45,14 @@ internal sealed class PlayerInfoConnection( private byte[]? GetMessageToSend() { var tank = entityManager.GetCurrentTankOfPlayer(player); - TankInfo? tankInfo = tank != null - ? new TankInfo(tank.Orientation, tank.Magazine.ToDisplayString(), tank.Position.ToPixelPosition(), tank.Moving) - : null; + + TankInfo? tankInfo = null; + if (tank != null) + { + var magazine = tank.ReloadingUntil > DateTime.Now ? "[ RELOADING ]" : tank.Magazine.ToDisplayString(); + tankInfo = new TankInfo(tank.Orientation, magazine, tank.Position.ToPixelPosition(), tank.Moving); + } + var info = new PlayerInfo(player.Name, player.Scores, player.Controls.ToDisplayString(), tankInfo); var response = JsonSerializer.SerializeToUtf8Bytes(info, _context.PlayerInfo); diff --git a/tanks-backend/TanksServer/Models/Tank.cs b/tanks-backend/TanksServer/Models/Tank.cs index 4d449a2..e9de961 100644 --- a/tanks-backend/TanksServer/Models/Tank.cs +++ b/tanks-backend/TanksServer/Models/Tank.cs @@ -31,4 +31,6 @@ internal sealed class Tank : IMapEntity public int Orientation => (int)Math.Round(Rotation * 16) % 16; public required Magazine Magazine { get; set; } + + public DateTime ReloadingUntil { get; set; } } From 21f7d1d5f4c239115b1ee0b18a7e4cb36e224c2b Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Mon, 29 Apr 2024 18:03:23 +0200 Subject: [PATCH 4/5] implement different kinds of power ups (two bullet types not implemented yet) --- .../TanksServer/GameLogic/CollectPowerUp.cs | 31 +++++++++++++++---- .../TanksServer/GameLogic/MapEntityManager.cs | 7 ++++- .../TanksServer/GameLogic/SpawnPowerUp.cs | 22 ++++++++++++- .../TanksServer/Models/IMapEntity.cs | 2 +- tanks-backend/TanksServer/Models/PowerUp.cs | 14 +++++++-- 5 files changed, 65 insertions(+), 11 deletions(-) diff --git a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs index 0195465..fe0ea1a 100644 --- a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace TanksServer.GameLogic; internal sealed class CollectPowerUp( @@ -22,14 +24,31 @@ internal sealed class CollectPowerUp( // now the tank overlaps the power up by at least 0.5 tiles - tank.Magazine = tank.Magazine with + switch (obj.Type) { - UsedBullets = 0, - Type = tank.Magazine.Type | MagazineType.Explosive - }; + case PowerUpType.MagazineTypeUpgrade: + if (obj.MagazineType == null) + throw new UnreachableException(); - if (tank.ReloadingUntil >= DateTime.Now) - tank.ReloadingUntil = DateTime.Now; + tank.Magazine = tank.Magazine with + { + Type = tank.Magazine.Type | obj.MagazineType.Value, + UsedBullets = 0 + }; + + if (tank.ReloadingUntil >= DateTime.Now) + tank.ReloadingUntil = DateTime.Now; + + break; + case PowerUpType.MagazineSizeUpgrade: + tank.Magazine = tank.Magazine with + { + MaxBullets = (byte)int.Clamp(tank.Magazine.MaxBullets + 1, 1, 32) + }; + break; + default: + throw new NotImplementedException(); + } tank.Owner.Scores.PowerUpsCollected++; return true; diff --git a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs index cd0ed6b..77e6f29 100644 --- a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs +++ b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs @@ -46,7 +46,12 @@ internal sealed class MapEntityManager( logger.LogInformation("Tank added for player {}", player.Name); } - public void SpawnPowerUp() => _powerUps.Add(new PowerUp(ChooseSpawnPosition())); + public void SpawnPowerUp(PowerUpType type, MagazineType? magazineType) => _powerUps.Add(new PowerUp + { + Position = ChooseSpawnPosition(), + Type = type, + MagazineType = magazineType + }); public void RemoveWhere(Predicate predicate) => _powerUps.RemoveWhere(predicate); diff --git a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs index f14a60e..41907a1 100644 --- a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace TanksServer.GameLogic; internal sealed class SpawnPowerUp( @@ -15,7 +17,25 @@ internal sealed class SpawnPowerUp( if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds) return ValueTask.CompletedTask; - entityManager.SpawnPowerUp(); + + var type = Random.Shared.Next(10) < 3 + ? PowerUpType.MagazineSizeUpgrade + : PowerUpType.MagazineTypeUpgrade; + + MagazineType? magazineType = type switch + { + PowerUpType.MagazineTypeUpgrade => Random.Shared.Next(0, 4) switch + { + 0 => MagazineType.Fast, + 1 => MagazineType.Explosive, + 2 => MagazineType.Smart, + 3 => MagazineType.Mine, + _ => throw new UnreachableException() + }, + _ => null + }; + + entityManager.SpawnPowerUp(type, magazineType); return ValueTask.CompletedTask; } } diff --git a/tanks-backend/TanksServer/Models/IMapEntity.cs b/tanks-backend/TanksServer/Models/IMapEntity.cs index 05b5700..a8ee53f 100644 --- a/tanks-backend/TanksServer/Models/IMapEntity.cs +++ b/tanks-backend/TanksServer/Models/IMapEntity.cs @@ -2,7 +2,7 @@ namespace TanksServer.Models; internal interface IMapEntity { - FloatPosition Position { get; set; } + FloatPosition Position { get; } PixelBounds Bounds { get; } } diff --git a/tanks-backend/TanksServer/Models/PowerUp.cs b/tanks-backend/TanksServer/Models/PowerUp.cs index 5fb8e56..6305a2d 100644 --- a/tanks-backend/TanksServer/Models/PowerUp.cs +++ b/tanks-backend/TanksServer/Models/PowerUp.cs @@ -2,9 +2,19 @@ using TanksServer.GameLogic; namespace TanksServer.Models; -internal sealed class PowerUp(FloatPosition position): IMapEntity +internal enum PowerUpType { - public FloatPosition Position { get; set; } = position; + MagazineTypeUpgrade, + MagazineSizeUpgrade +} + +internal sealed class PowerUp: IMapEntity +{ + public required FloatPosition Position { get; init; } public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize); + + public required PowerUpType Type { get; init; } + + public MagazineType? MagazineType { get; init; } } From 0e01ff0fb9a19e0120c7b269d4787895bdd381bd Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Mon, 29 Apr 2024 21:52:50 +0200 Subject: [PATCH 5/5] implement smart bullet --- .../TanksServer/GameLogic/CollectPowerUp.cs | 2 +- .../TanksServer/GameLogic/GameRules.cs | 2 ++ .../TanksServer/GameLogic/MapEntityManager.cs | 3 +- .../TanksServer/GameLogic/MoveBullets.cs | 36 +++++++++++++++++-- .../TanksServer/GameLogic/SpawnPowerUp.cs | 5 ++- tanks-backend/TanksServer/Models/Bullet.cs | 4 ++- tanks-backend/TanksServer/Models/Magazine.cs | 11 +++--- .../TanksServer/Models/PositionHelpers.cs | 1 - tanks-backend/TanksServer/appsettings.json | 11 +++--- 9 files changed, 54 insertions(+), 21 deletions(-) diff --git a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs index fe0ea1a..e3a68a5 100644 --- a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs @@ -47,7 +47,7 @@ internal sealed class CollectPowerUp( }; break; default: - throw new NotImplementedException(); + throw new UnreachableException(); } tank.Owner.Scores.PowerUpsCollected++; diff --git a/tanks-backend/TanksServer/GameLogic/GameRules.cs b/tanks-backend/TanksServer/GameLogic/GameRules.cs index 5c4a98f..2284464 100644 --- a/tanks-backend/TanksServer/GameLogic/GameRules.cs +++ b/tanks-backend/TanksServer/GameLogic/GameRules.cs @@ -25,4 +25,6 @@ internal sealed class GameRules public byte MagazineSize { get; set; } = 5; public int ReloadDelayMs { get; set; } = 3000; + + public double SmartBulletInertia { get; set; } = 1; } diff --git a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs index 77e6f29..42d30ff 100644 --- a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs +++ b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs @@ -27,7 +27,8 @@ internal sealed class MapEntityManager( IsExplosive = type.HasFlag(MagazineType.Explosive), Timeout = DateTime.Now + _bulletTimeout, OwnerCollisionAfter = DateTime.Now + TimeSpan.FromSeconds(1), - Speed = speed + Speed = speed, + IsSmart = type.HasFlag(MagazineType.Smart) }); } diff --git a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs index 755e454..96bde60 100644 --- a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs +++ b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs @@ -1,7 +1,12 @@ namespace TanksServer.GameLogic; -internal sealed class MoveBullets(MapEntityManager entityManager) : ITickStep +internal sealed class MoveBullets( + MapEntityManager entityManager, + IOptions options +) : ITickStep { + private readonly double _smartBulletInertia = options.Value.SmartBulletInertia; + public ValueTask TickAsync(TimeSpan delta) { foreach (var bullet in entityManager.Bullets) @@ -10,8 +15,15 @@ internal sealed class MoveBullets(MapEntityManager entityManager) : ITickStep return ValueTask.CompletedTask; } - private static void MoveBullet(Bullet bullet, TimeSpan delta) + private void MoveBullet(Bullet bullet, TimeSpan delta) { + if (bullet.IsSmart && TryGetSmartRotation(bullet.Position, bullet.Owner, out var wantedRotation)) + { + var inertiaFactor = _smartBulletInertia * delta.TotalSeconds; + var difference = wantedRotation - bullet.Rotation; + bullet.Rotation += difference * inertiaFactor; + } + var speed = bullet.Speed * delta.TotalSeconds; var angle = bullet.Rotation * 2 * Math.PI; bullet.Position = new FloatPosition( @@ -19,4 +31,24 @@ internal sealed class MoveBullets(MapEntityManager entityManager) : ITickStep bullet.Position.Y - Math.Cos(angle) * speed ); } + + private bool TryGetSmartRotation(FloatPosition position, Player bulletOwner, out double rotation) + { + var nearestEnemy = entityManager.Tanks + .Where(t => t.Owner != bulletOwner) + .MinBy(t => position.Distance(t.Position)); + + if (nearestEnemy == null) + { + rotation = double.NaN; + return false; + } + + var rotationRadians = Math.Atan2( + y: nearestEnemy.Position.Y - position.Y, + x: nearestEnemy.Position.X - position.X + ) + (Math.PI / 2); + rotation = rotationRadians / (2 * Math.PI); + return true; + } } diff --git a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs index 41907a1..56c7ae7 100644 --- a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs @@ -18,18 +18,17 @@ internal sealed class SpawnPowerUp( return ValueTask.CompletedTask; - var type = Random.Shared.Next(10) < 3 + var type = Random.Shared.Next(4) == 0 ? PowerUpType.MagazineSizeUpgrade : PowerUpType.MagazineTypeUpgrade; MagazineType? magazineType = type switch { - PowerUpType.MagazineTypeUpgrade => Random.Shared.Next(0, 4) switch + PowerUpType.MagazineTypeUpgrade => Random.Shared.Next(0, 3) switch { 0 => MagazineType.Fast, 1 => MagazineType.Explosive, 2 => MagazineType.Smart, - 3 => MagazineType.Mine, _ => throw new UnreachableException() }, _ => null diff --git a/tanks-backend/TanksServer/Models/Bullet.cs b/tanks-backend/TanksServer/Models/Bullet.cs index f685741..9e17066 100644 --- a/tanks-backend/TanksServer/Models/Bullet.cs +++ b/tanks-backend/TanksServer/Models/Bullet.cs @@ -4,7 +4,7 @@ internal sealed class Bullet : IMapEntity { public required Player Owner { get; init; } - public required double Rotation { get; init; } + public required double Rotation { get; set; } public required FloatPosition Position { get; set; } @@ -17,4 +17,6 @@ internal sealed class Bullet : IMapEntity internal required DateTime OwnerCollisionAfter { get; init; } public required double Speed { get; init; } + + public required bool IsSmart { get; init; } } diff --git a/tanks-backend/TanksServer/Models/Magazine.cs b/tanks-backend/TanksServer/Models/Magazine.cs index 8994e61..b3143b5 100644 --- a/tanks-backend/TanksServer/Models/Magazine.cs +++ b/tanks-backend/TanksServer/Models/Magazine.cs @@ -21,16 +21,13 @@ internal readonly record struct Magazine(MagazineType Type, byte UsedBullets, by var sb = new StringBuilder(); if (Type.HasFlag(MagazineType.Fast)) - sb.Append('»'); + sb.Append("» "); if (Type.HasFlag(MagazineType.Explosive)) - sb.Append('*'); + sb.Append("* "); if (Type.HasFlag(MagazineType.Smart)) - sb.Append('@'); + sb.Append("@ "); if (Type.HasFlag(MagazineType.Mine)) - sb.Append('\u263c'); - - if (sb.Length > 0) - sb.Append(' '); + sb.Append("\u263c "); sb.Append("[ "); for (var i = 0; i < UsedBullets; i++) diff --git a/tanks-backend/TanksServer/Models/PositionHelpers.cs b/tanks-backend/TanksServer/Models/PositionHelpers.cs index 5af5073..223a472 100644 --- a/tanks-backend/TanksServer/Models/PositionHelpers.cs +++ b/tanks-backend/TanksServer/Models/PositionHelpers.cs @@ -22,7 +22,6 @@ internal static class PositionHelpers public static FloatPosition ToFloatPosition(this PixelPosition position) => new(position.X, position.Y); - public static double Distance(this FloatPosition p1, FloatPosition p2) => Math.Sqrt( Math.Pow(p1.X - p2.X, 2) + diff --git a/tanks-backend/TanksServer/appsettings.json b/tanks-backend/TanksServer/appsettings.json index 045fd04..dd8aa6d 100644 --- a/tanks-backend/TanksServer/appsettings.json +++ b/tanks-backend/TanksServer/appsettings.json @@ -21,15 +21,16 @@ }, "GameRules": { "DestructibleWalls": true, - "PowerUpSpawnChance": 0.1, - "MaxPowerUpCount": 15, - "BulletTimeoutMs": 30000, + "PowerUpSpawnChance": 0.2, + "MaxPowerUpCount": 5, + "BulletTimeoutMs": 20000, "SpawnDelayMs": 3000, "IdleTimeoutMs": 30000, - "MoveSpeed": 37.5, + "MoveSpeed": 40, "TurnSpeed": 0.5, "ShootDelayMs": 450, - "BulletSpeed": 75 + "BulletSpeed": 75, + "SmartBulletHomingSpeed": 1.5 }, "Host": { "EnableServicePointDisplay": true,