diff --git a/tank-frontend/src/PlayerInfo.tsx b/tank-frontend/src/PlayerInfo.tsx index 4ab04bc..7f54c06 100644 --- a/tank-frontend/src/PlayerInfo.tsx +++ b/tank-frontend/src/PlayerInfo.tsx @@ -46,29 +46,35 @@ export default function PlayerInfo({player}: { player: string }) { if (!lastJsonMessage || readyState !== ReadyState.OPEN) return <>; + let position = ''; + if (lastJsonMessage.tank) + position = `(${Math.round(lastJsonMessage.tank.position.x)}|${Math.round(lastJsonMessage.tank.position.y)})`; + return

- Playing as {lastJsonMessage.name} + Playing as {lastJsonMessage.player.name}

- + + + + + + + + + + + + + + - - - - - - - - - - - - +
; diff --git a/tank-frontend/src/serverCalls.tsx b/tank-frontend/src/serverCalls.tsx index 83e4f2d..57ae781 100644 --- a/tank-frontend/src/serverCalls.tsx +++ b/tank-frontend/src/serverCalls.tsx @@ -14,24 +14,32 @@ export type Scores = { readonly pixelsMoved: number; }; -export type Player = { - readonly name: string; - readonly scores: Scores; -}; - -type TankInfo = { - readonly magazine: string; +type Tank = { readonly position: { x: number; y: number }; readonly orientation: number; readonly moving: boolean; + readonly bulletStats: BulletStats; + readonly reloadingUntil: string; + readonly nextShotAfter: string; + readonly magazine: { + readonly empty: boolean; + readonly usedBullets: number; + readonly maxBullets: number; + readonly displayString: string; + }; +} + +export type Player = { + readonly name: string; + readonly scores: Scores; + readonly openConnections: number; + readonly lastInput: string; } export type PlayerInfoMessage = { - readonly name: string; - readonly scores: Scores; + readonly player: Player; readonly controls: string; - readonly tank?: TankInfo; - readonly openConnections: number; + readonly tank?: Tank; } export type MapInfo = { @@ -40,6 +48,13 @@ export type MapInfo = { readonly preview: string; } +export type BulletStats = { + speed: number; + acceleration: number, + explosive: boolean, + smart: boolean +}; + export function useMyWebSocket(url: string, options: Options = {}) { return useWebSocket(url, { shouldReconnect: () => true, diff --git a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs index 3b3571f..840c043 100644 --- a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs @@ -2,19 +2,27 @@ using System.Diagnostics; namespace TanksServer.GameLogic; -internal sealed class CollectPowerUp( - MapEntityManager entityManager -) : ITickStep +internal sealed class CollectPowerUp : ITickStep { - private readonly Predicate _collectPredicate = b => TryCollect(b, entityManager.Tanks); + private readonly Predicate _collectPredicate; + private readonly GameRules _rules; + private readonly MapEntityManager _entityManager; + + public CollectPowerUp(MapEntityManager entityManager, + IOptions options) + { + _entityManager = entityManager; + _rules = options.Value; + _collectPredicate = b => TryCollect(b, entityManager.Tanks); + } public ValueTask TickAsync(TimeSpan delta) { - entityManager.RemoveWhere(_collectPredicate); + _entityManager.RemoveWhere(_collectPredicate); return ValueTask.CompletedTask; } - private static bool TryCollect(PowerUp powerUp, IEnumerable tanks) + private bool TryCollect(PowerUp powerUp, IEnumerable tanks) { var position = powerUp.Position; foreach (var tank in tanks) @@ -34,32 +42,38 @@ internal sealed class CollectPowerUp( return false; } - private static void ApplyPowerUpEffect(PowerUp powerUp, Tank tank) + private void ApplyPowerUpEffect(PowerUp powerUp, Tank tank) { switch (powerUp.Type) { - case PowerUpType.MagazineType: - if (powerUp.MagazineType == null) - throw new UnreachableException(); - - tank.Magazine = tank.Magazine with - { - Type = tank.Magazine.Type | powerUp.MagazineType.Value, - UsedBullets = 0 - }; - - if (tank.ReloadingUntil >= DateTime.Now) - tank.ReloadingUntil = DateTime.Now; - - break; case PowerUpType.MagazineSize: - tank.Magazine = tank.Magazine with + tank.MaxBullets = (byte)int.Clamp(tank.MaxBullets + 1, 1, 32); + break; + + case PowerUpType.BulletAcceleration: + tank.BulletStats = tank.BulletStats with { - MaxBullets = (byte)int.Clamp(tank.Magazine.MaxBullets + 1, 1, 32) + Acceleration = tank.BulletStats.Acceleration * _rules.BulletAccelerationUpgradeStrength }; break; + + case PowerUpType.ExplosiveBullets: + tank.BulletStats = tank.BulletStats with { Explosive = true }; + break; + + case PowerUpType.SmartBullets: + tank.BulletStats = tank.BulletStats with { Smart = true }; + break; + + case PowerUpType.BulletSpeed: + tank.BulletStats = tank.BulletStats with + { + Speed = tank.BulletStats.Speed * _rules.BulletSpeedUpgradeStrength + }; + break; + default: - throw new UnreachableException(); + throw new NotImplementedException($"unknown type {powerUp.Type}"); } } } diff --git a/tanks-backend/TanksServer/GameLogic/CollideBullets.cs b/tanks-backend/TanksServer/GameLogic/CollideBullets.cs index 4be0dca..c75fe6a 100644 --- a/tanks-backend/TanksServer/GameLogic/CollideBullets.cs +++ b/tanks-backend/TanksServer/GameLogic/CollideBullets.cs @@ -35,7 +35,7 @@ internal sealed class CollideBullets : ITickStep if (bullet.Timeout > DateTime.Now) return false; - ExplodeAt(bullet.Position.ToPixelPosition(), bullet.IsExplosive, bullet.Owner); + ExplodeAt(bullet.Position.ToPixelPosition(), bullet.Stats.Explosive, bullet.Owner); return true; } @@ -45,7 +45,7 @@ internal sealed class CollideBullets : ITickStep if (!_map.Current.IsWall(pixel)) return false; - ExplodeAt(pixel, bullet.IsExplosive, bullet.Owner); + ExplodeAt(pixel, bullet.Stats.Explosive, bullet.Owner); return true; } @@ -55,7 +55,7 @@ internal sealed class CollideBullets : ITickStep if (hitTank == null) return false; - ExplodeAt(bullet.Position.ToPixelPosition(), bullet.IsExplosive, bullet.Owner); + ExplodeAt(bullet.Position.ToPixelPosition(), bullet.Stats.Explosive, bullet.Owner); return true; } diff --git a/tanks-backend/TanksServer/GameLogic/GameRules.cs b/tanks-backend/TanksServer/GameLogic/GameRules.cs index 3660671..2d338b3 100644 --- a/tanks-backend/TanksServer/GameLogic/GameRules.cs +++ b/tanks-backend/TanksServer/GameLogic/GameRules.cs @@ -28,5 +28,7 @@ internal sealed class GameRules public double SmartBulletInertia { get; set; } = 1; - public double FastBulletAcceleration { get; set; } = 0.25; + public double BulletAccelerationUpgradeStrength { get; set; } = 0.1; + + public double BulletSpeedUpgradeStrength { get; set; } = 0.1; } diff --git a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs index 3b1f4f6..31cb28c 100644 --- a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs +++ b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs @@ -15,19 +15,17 @@ internal sealed class MapEntityManager( public IEnumerable Tanks => _playerTanks.Values; public IEnumerable PowerUps => _powerUps; - public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, MagazineType type) + public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, BulletStats stats) { _bullets.Add(new Bullet { Owner = tankOwner, Position = position, Rotation = rotation, - IsExplosive = type.HasFlag(MagazineType.Explosive), Timeout = DateTime.Now + _bulletTimeout, OwnerCollisionAfter = DateTime.Now + TimeSpan.FromSeconds(1), Speed = _rules.BulletSpeed, - IsSmart = type.HasFlag(MagazineType.Smart), - Acceleration = type.HasFlag(MagazineType.Fast) ? _rules.FastBulletAcceleration : 0d + Stats = stats }); } @@ -35,24 +33,23 @@ internal sealed class MapEntityManager( public void SpawnTank(Player player, FloatPosition position) { - var tank = new Tank + var tank = new Tank(player) { - Owner = player, Position = position, Rotation = Random.Shared.NextDouble(), - Magazine = new Magazine(MagazineType.Basic, 0, _rules.MagazineSize) + MaxBullets = _rules.MagazineSize, + BulletStats =new BulletStats(_rules.BulletSpeed, 0, false, false) }; _playerTanks[player] = tank; logger.LogInformation("Tank added for player {}", player.Name); } - public void SpawnPowerUp(FloatPosition position, PowerUpType type, MagazineType? magazineType) + public void SpawnPowerUp(FloatPosition position, PowerUpType type) { var powerUp = new PowerUp { Position = position, - Type = type, - MagazineType = magazineType + Type = type }; _powerUps.Add(powerUp); } diff --git a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs index d303d84..71e776f 100644 --- a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs +++ b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs @@ -17,14 +17,15 @@ internal sealed class MoveBullets( private void MoveBullet(Bullet bullet, TimeSpan delta) { - if (bullet.IsSmart && TryGetSmartRotation(bullet.Position, bullet.Owner, out var wantedRotation)) + if (bullet.Stats.Smart && TryGetSmartRotation(bullet.Position, bullet.Owner, out var wantedRotation)) { var inertiaFactor = _smartBulletInertia * delta.TotalSeconds; var difference = wantedRotation - bullet.Rotation; bullet.Rotation += difference * inertiaFactor; } - bullet.Speed *= 1 + (bullet.Acceleration * delta.TotalSeconds); + bullet.Speed = double.Clamp(bullet.Speed * (1 + (bullet.Stats.Acceleration * delta.TotalSeconds)), 0d, + MapService.TileSize * 10); var speed = bullet.Speed * delta.TotalSeconds; var angle = bullet.Rotation * 2 * Math.PI; diff --git a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs index 9df8a64..3014c45 100644 --- a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs +++ b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs @@ -26,24 +26,17 @@ internal sealed class ShootFromTanks( if (tank.ReloadingUntil >= now) return; - if (tank.Magazine.Empty) + if (tank.UsedBullets >= tank.MaxBullets) { tank.ReloadingUntil = now.AddMilliseconds(_config.ReloadDelayMs); - tank.Magazine = tank.Magazine with - { - UsedBullets = 0, - Type = MagazineType.Basic - }; + tank.UsedBullets = 0; return; } tank.NextShotAfter = now.AddMilliseconds(_config.ShootDelayMs); - tank.Magazine = tank.Magazine with - { - UsedBullets = (byte)(tank.Magazine.UsedBullets + 1) - }; + tank.UsedBullets++; tank.Owner.Scores.ShotsFired++; - entityManager.SpawnBullet(tank.Owner, tank.Position, tank.Orientation / 16d, tank.Magazine.Type); + entityManager.SpawnBullet(tank.Owner, tank.Position, tank.Orientation / 16d, tank.BulletStats); } } diff --git a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs index 72b0592..3107b71 100644 --- a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs +++ b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs @@ -18,25 +18,9 @@ internal sealed class SpawnPowerUp( if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds) return ValueTask.CompletedTask; - - var type = Random.Shared.Next(4) == 0 - ? PowerUpType.MagazineSize - : PowerUpType.MagazineType; - - MagazineType? magazineType = type switch - { - PowerUpType.MagazineType => Random.Shared.Next(0, 3) switch - { - 0 => MagazineType.Fast, - 1 => MagazineType.Explosive, - 2 => MagazineType.Smart, - _ => throw new UnreachableException() - }, - _ => null - }; - + var type = (PowerUpType)Random.Shared.Next((int)Enum.GetValues().Max()); var position = emptyTileFinder.ChooseEmptyTile().GetCenter().ToFloatPosition(); - entityManager.SpawnPowerUp(position, type, magazineType); + entityManager.SpawnPowerUp(position, type); return ValueTask.CompletedTask; } } diff --git a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs index 6d47060..c6125ec 100644 --- a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs @@ -14,12 +14,12 @@ internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawSt { foreach (var powerUp in entityManager.PowerUps) { - var sprite = powerUp switch + var sprite = powerUp.Type switch { - { Type: PowerUpType.MagazineSize } => _magazineSprite, - { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Smart } => _smartSprite, - { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Explosive } => _explosiveSprite, - { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Fast } => _fastSprite, + PowerUpType.MagazineSize => _magazineSprite, + PowerUpType.BulletAcceleration or PowerUpType.BulletSpeed => _fastSprite, + PowerUpType.SmartBullets => _smartSprite, + PowerUpType.ExplosiveBullets => _explosiveSprite, _ => _genericSprite }; diff --git a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs index 04661c3..a81d2c4 100644 --- a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs +++ b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs @@ -47,20 +47,7 @@ internal sealed class PlayerInfoConnection private async ValueTask?> GenerateMessageAsync() { var tank = _entityManager.GetCurrentTankOfPlayer(_player); - - 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, - _player.OpenConnections); + var info = new PlayerInfo(_player, _player.Controls.ToDisplayString(), tank); _tempStream.Position = 0; await JsonSerializer.SerializeAsync(_tempStream, info, AppSerializerContext.Default.PlayerInfo); @@ -85,3 +72,9 @@ internal sealed class PlayerInfoConnection Interlocked.Exchange(ref _lastMessage, data)?.Dispose(); } } + +internal record struct PlayerInfo( + Player Player, + string Controls, + Tank? Tank +); diff --git a/tanks-backend/TanksServer/Models/Bullet.cs b/tanks-backend/TanksServer/Models/Bullet.cs index c5f8d1d..aed3ddf 100644 --- a/tanks-backend/TanksServer/Models/Bullet.cs +++ b/tanks-backend/TanksServer/Models/Bullet.cs @@ -8,8 +8,6 @@ internal sealed class Bullet : IMapEntity public required FloatPosition Position { get; set; } - public required bool IsExplosive { get; init; } - public required DateTime Timeout { get; init; } public PixelBounds Bounds => new(Position.ToPixelPosition(), Position.ToPixelPosition()); @@ -18,7 +16,5 @@ internal sealed class Bullet : IMapEntity public required double Speed { get; set; } - public required double Acceleration { get; init; } - - public required bool IsSmart { get; init; } + public required BulletStats Stats { get; init; } } diff --git a/tanks-backend/TanksServer/Models/Magazine.cs b/tanks-backend/TanksServer/Models/Magazine.cs deleted file mode 100644 index febd5b8..0000000 --- a/tanks-backend/TanksServer/Models/Magazine.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text; - -namespace TanksServer.Models; - -[Flags] -internal enum MagazineType -{ - Basic = 0, - Fast = 1 << 0, - Explosive = 1 << 1, - Smart = 1 << 2, -} - -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("@ "); - - 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/PlayerInfo.cs b/tanks-backend/TanksServer/Models/PlayerInfo.cs deleted file mode 100644 index a8545f3..0000000 --- a/tanks-backend/TanksServer/Models/PlayerInfo.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace TanksServer.Models; - -internal record struct TankInfo( - int Orientation, - string Magazine, - PixelPosition Position, - bool Moving -); - -internal record struct PlayerInfo( - string Name, - Scores Scores, - string Controls, - TankInfo? Tank, - int OpenConnections -); diff --git a/tanks-backend/TanksServer/Models/PowerUp.cs b/tanks-backend/TanksServer/Models/PowerUp.cs index a8adb25..318a3a4 100644 --- a/tanks-backend/TanksServer/Models/PowerUp.cs +++ b/tanks-backend/TanksServer/Models/PowerUp.cs @@ -4,8 +4,11 @@ namespace TanksServer.Models; internal enum PowerUpType { - MagazineType, - MagazineSize + MagazineSize, + BulletSpeed, + BulletAcceleration, + ExplosiveBullets, + SmartBullets, } internal sealed class PowerUp: IMapEntity @@ -15,6 +18,4 @@ internal sealed class PowerUp: IMapEntity public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize); public required PowerUpType Type { get; init; } - - public MagazineType? MagazineType { get; init; } } diff --git a/tanks-backend/TanksServer/Models/Tank.cs b/tanks-backend/TanksServer/Models/Tank.cs index e9de961..ee955ea 100644 --- a/tanks-backend/TanksServer/Models/Tank.cs +++ b/tanks-backend/TanksServer/Models/Tank.cs @@ -1,13 +1,14 @@ using System.Diagnostics; +using System.Text.Json.Serialization; using TanksServer.GameLogic; namespace TanksServer.Models; -internal sealed class Tank : IMapEntity +internal sealed class Tank(Player owner) : IMapEntity { private double _rotation; - public required Player Owner { get; init; } + [JsonIgnore] public Player Owner { get; } = owner; public double Rotation { @@ -26,11 +27,17 @@ internal sealed class Tank : IMapEntity public required FloatPosition Position { get; set; } - public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize); + [JsonIgnore] public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize); public int Orientation => (int)Math.Round(Rotation * 16) % 16; - public required Magazine Magazine { get; set; } + public int UsedBullets { get; set; } + + public int MaxBullets { get; set; } public DateTime ReloadingUntil { get; set; } + + public required BulletStats BulletStats { get; set; } } + +internal sealed record class BulletStats(double Speed, double Acceleration, bool Explosive, bool Smart);