move backend to subfolder
This commit is contained in:
parent
d4d1f2f981
commit
8d09663eff
80 changed files with 98 additions and 88 deletions
30
tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs
Normal file
30
tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class CollectPowerUp(
|
||||
MapEntityManager entityManager
|
||||
) : ITickStep
|
||||
{
|
||||
public Task TickAsync(TimeSpan delta)
|
||||
{
|
||||
entityManager.RemoveWhere(TryCollect);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool TryCollect(PowerUp obj)
|
||||
{
|
||||
var position = obj.Position;
|
||||
foreach (var tank in entityManager.Tanks)
|
||||
{
|
||||
var (topLeft, bottomRight) = tank.Bounds;
|
||||
if (position.X < topLeft.X || position.X > bottomRight.X ||
|
||||
position.Y < topLeft.Y || position.Y > bottomRight.Y)
|
||||
continue;
|
||||
|
||||
// this works because now the tank overlaps the power up
|
||||
tank.ExplosiveBullets += 10;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
88
tanks-backend/TanksServer/GameLogic/CollideBullets.cs
Normal file
88
tanks-backend/TanksServer/GameLogic/CollideBullets.cs
Normal file
|
@ -0,0 +1,88 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class CollideBullets(
|
||||
MapEntityManager entityManager,
|
||||
MapService map,
|
||||
IOptions<GameRules> options,
|
||||
TankSpawnQueue tankSpawnQueue
|
||||
) : ITickStep
|
||||
{
|
||||
private const int ExplosionRadius = 3;
|
||||
|
||||
public Task TickAsync(TimeSpan _)
|
||||
{
|
||||
entityManager.RemoveBulletsWhere(BulletHitsTank);
|
||||
entityManager.RemoveBulletsWhere(TryHitAndDestroyWall);
|
||||
entityManager.RemoveBulletsWhere(TimeoutBullet);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool TimeoutBullet(Bullet bullet)
|
||||
{
|
||||
if (bullet.Timeout > DateTime.Now)
|
||||
return false;
|
||||
|
||||
var radius = bullet.IsExplosive ? ExplosionRadius : 0;
|
||||
ExplodeAt(bullet.Position.ToPixelPosition(), radius, bullet.Owner);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryHitAndDestroyWall(Bullet bullet)
|
||||
{
|
||||
var pixel = bullet.Position.ToPixelPosition();
|
||||
if (!map.Current.IsWall(pixel))
|
||||
return false;
|
||||
|
||||
var radius = bullet.IsExplosive ? ExplosionRadius : 0;
|
||||
ExplodeAt(pixel, radius, bullet.Owner);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool BulletHitsTank(Bullet bullet)
|
||||
{
|
||||
if (!TryHitTankAt(bullet.Position, bullet.Owner))
|
||||
return false;
|
||||
|
||||
if (bullet.IsExplosive)
|
||||
ExplodeAt(bullet.Position.ToPixelPosition(), ExplosionRadius, bullet.Owner);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryHitTankAt(FloatPosition position, Player owner)
|
||||
{
|
||||
foreach (var tank in entityManager.Tanks)
|
||||
{
|
||||
var (topLeft, bottomRight) = tank.Bounds;
|
||||
if (position.X < topLeft.X || position.X > bottomRight.X ||
|
||||
position.Y < topLeft.Y || position.Y > bottomRight.Y)
|
||||
continue;
|
||||
|
||||
if (owner != tank.Owner)
|
||||
owner.Scores.Kills++;
|
||||
tank.Owner.Scores.Deaths++;
|
||||
|
||||
entityManager.Remove(tank);
|
||||
tankSpawnQueue.EnqueueForDelayedSpawn(tank.Owner);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ExplodeAt(PixelPosition pixel, int i, Player owner)
|
||||
{
|
||||
for (var x = pixel.X - i; x <= pixel.X + i; x++)
|
||||
for (var y = pixel.Y - i; y <= pixel.Y + i; y++)
|
||||
{
|
||||
var offsetPixel = new PixelPosition(x, y);
|
||||
if (options.Value.DestructibleWalls)
|
||||
{
|
||||
map.Current.DestroyWallAt(offsetPixel);
|
||||
owner.Scores.WallsDestroyed++;
|
||||
}
|
||||
|
||||
TryHitTankAt(offsetPixel.ToFloatPosition(), owner);
|
||||
}
|
||||
}
|
||||
}
|
24
tanks-backend/TanksServer/GameLogic/GameRules.cs
Normal file
24
tanks-backend/TanksServer/GameLogic/GameRules.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class GameRules
|
||||
{
|
||||
public bool DestructibleWalls { get; set; } = true;
|
||||
|
||||
public double PowerUpSpawnChance { get; set; }
|
||||
|
||||
public int MaxPowerUpCount { get; set; } = int.MaxValue;
|
||||
|
||||
public int BulletTimeoutMs { get; set; } = int.MaxValue;
|
||||
|
||||
public double MoveSpeed { get; set; }
|
||||
|
||||
public double TurnSpeed { get; set; }
|
||||
|
||||
public double ShootDelayMs { get; set; }
|
||||
|
||||
public double BulletSpeed { get; set; }
|
||||
|
||||
public int SpawnDelayMs { get; set; }
|
||||
|
||||
public int IdleTimeoutMs { get; set; }
|
||||
}
|
58
tanks-backend/TanksServer/GameLogic/GameTickWorker.cs
Normal file
58
tanks-backend/TanksServer/GameLogic/GameTickWorker.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class GameTickWorker(
|
||||
IEnumerable<ITickStep> steps,
|
||||
IHostApplicationLifetime lifetime,
|
||||
ILogger<GameTickWorker> logger
|
||||
) : IHostedService, IDisposable
|
||||
{
|
||||
private readonly CancellationTokenSource _cancellation = new();
|
||||
private readonly List<ITickStep> _steps = steps.ToList();
|
||||
private Task? _run;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellation.Dispose();
|
||||
_run?.Dispose();
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_run = RunAsync();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _cancellation.CancelAsync();
|
||||
if (_run != null) await _run;
|
||||
}
|
||||
|
||||
private async Task RunAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
while (!_cancellation.IsCancellationRequested)
|
||||
{
|
||||
logger.LogTrace("since last frame: {}", sw.Elapsed);
|
||||
|
||||
var delta = sw.Elapsed;
|
||||
sw.Restart();
|
||||
|
||||
foreach (var step in _steps)
|
||||
await step.TickAsync(delta);
|
||||
|
||||
await Task.Delay(1);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "game tick service crashed");
|
||||
lifetime.StopApplication();
|
||||
}
|
||||
}
|
||||
}
|
6
tanks-backend/TanksServer/GameLogic/ITickStep.cs
Normal file
6
tanks-backend/TanksServer/GameLogic/ITickStep.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
public interface ITickStep
|
||||
{
|
||||
Task TickAsync(TimeSpan delta);
|
||||
}
|
70
tanks-backend/TanksServer/GameLogic/MapEntityManager.cs
Normal file
70
tanks-backend/TanksServer/GameLogic/MapEntityManager.cs
Normal file
|
@ -0,0 +1,70 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class MapEntityManager(
|
||||
ILogger<MapEntityManager> logger,
|
||||
MapService map,
|
||||
IOptions<GameRules> options
|
||||
)
|
||||
{
|
||||
private readonly HashSet<Bullet> _bullets = [];
|
||||
private readonly HashSet<Tank> _tanks = [];
|
||||
private readonly HashSet<PowerUp> _powerUps = [];
|
||||
private readonly TimeSpan _bulletTimeout = TimeSpan.FromMilliseconds(options.Value.BulletTimeoutMs);
|
||||
|
||||
public IEnumerable<Bullet> Bullets => _bullets;
|
||||
public IEnumerable<Tank> Tanks => _tanks;
|
||||
public IEnumerable<PowerUp> PowerUps => _powerUps;
|
||||
|
||||
public IEnumerable<IMapEntity> AllEntities => Bullets
|
||||
.Cast<IMapEntity>()
|
||||
.Concat(Tanks)
|
||||
.Concat(PowerUps);
|
||||
|
||||
public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, bool isExplosive)
|
||||
=> _bullets.Add(new Bullet(tankOwner, position, rotation, isExplosive, DateTime.Now + _bulletTimeout));
|
||||
|
||||
public void RemoveBulletsWhere(Predicate<Bullet> predicate) => _bullets.RemoveWhere(predicate);
|
||||
|
||||
public void SpawnTank(Player player)
|
||||
{
|
||||
_tanks.Add(new Tank(player, ChooseSpawnPosition())
|
||||
{
|
||||
Rotation = Random.Shared.NextDouble()
|
||||
});
|
||||
logger.LogInformation("Tank added for player {}", player.Id);
|
||||
}
|
||||
|
||||
public void SpawnPowerUp() => _powerUps.Add(new PowerUp(ChooseSpawnPosition()));
|
||||
|
||||
public void RemoveWhere(Predicate<PowerUp> predicate) => _powerUps.RemoveWhere(predicate);
|
||||
|
||||
public void Remove(Tank tank)
|
||||
{
|
||||
logger.LogInformation("Tank removed for player {}", tank.Owner.Id);
|
||||
_tanks.Remove(tank);
|
||||
}
|
||||
|
||||
public FloatPosition ChooseSpawnPosition()
|
||||
{
|
||||
Dictionary<TilePosition, double> candidates = [];
|
||||
|
||||
for (ushort x = 1; x < MapService.TilesPerRow - 1; x++)
|
||||
for (ushort y = 1; y < MapService.TilesPerColumn - 1; y++)
|
||||
{
|
||||
var tile = new TilePosition(x, y);
|
||||
if (map.Current.IsWall(tile))
|
||||
continue;
|
||||
|
||||
var tilePixelCenter = tile.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
|
||||
|
||||
var minDistance = AllEntities
|
||||
.Select(entity => entity.Position.Distance(tilePixelCenter))
|
||||
.Aggregate(double.MaxValue, Math.Min);
|
||||
|
||||
candidates.Add(tile, minDistance);
|
||||
}
|
||||
|
||||
var min = candidates.MaxBy(pair => pair.Value).Key;
|
||||
return min.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
|
||||
}
|
||||
}
|
108
tanks-backend/TanksServer/GameLogic/MapService.cs
Normal file
108
tanks-backend/TanksServer/GameLogic/MapService.cs
Normal file
|
@ -0,0 +1,108 @@
|
|||
using System.IO;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class MapService
|
||||
{
|
||||
public const ushort TilesPerRow = 44;
|
||||
public const ushort TilesPerColumn = 20;
|
||||
public const ushort TileSize = 8;
|
||||
public const ushort PixelsPerRow = TilesPerRow * TileSize;
|
||||
public const ushort PixelsPerColumn = TilesPerColumn * TileSize;
|
||||
|
||||
private readonly Dictionary<string, bool[,]> _maps = new();
|
||||
|
||||
public IEnumerable<string> MapNames => _maps.Keys;
|
||||
|
||||
public Map Current { get; private set; }
|
||||
|
||||
public MapService()
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles("./assets/maps/", "*.txt"))
|
||||
LoadMapString(file);
|
||||
foreach (var file in Directory.EnumerateFiles("./assets/maps/", "*.png"))
|
||||
LoadMapPng(file);
|
||||
|
||||
var chosenMapIndex = Random.Shared.Next(_maps.Count);
|
||||
var chosenMapName = _maps.Keys.Skip(chosenMapIndex).First();
|
||||
Current = new Map(chosenMapName, _maps[chosenMapName]);
|
||||
}
|
||||
|
||||
private void LoadMapPng(string file)
|
||||
{
|
||||
using var image = Image.Load<Rgba32>(file);
|
||||
|
||||
if (image.Width != PixelsPerRow || image.Height != PixelsPerColumn)
|
||||
throw new FileLoadException($"invalid image size in file {file}");
|
||||
|
||||
var whitePixel = new Rgba32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
|
||||
var walls = new bool[PixelsPerRow, PixelsPerColumn];
|
||||
|
||||
for (var y = 0; y < image.Height; y++)
|
||||
for (var x = 0; x < image.Width; x++)
|
||||
walls[x, y] = image[x, y] == whitePixel;
|
||||
|
||||
_maps.Add(Path.GetFileName(file), walls);
|
||||
}
|
||||
|
||||
private void LoadMapString(string file)
|
||||
{
|
||||
var map = File.ReadAllText(file).ReplaceLineEndings(string.Empty).Trim();
|
||||
if (map.Length != TilesPerColumn * TilesPerRow)
|
||||
throw new FileLoadException($"cannot load map {file}: invalid length");
|
||||
|
||||
var walls = new bool[PixelsPerRow, PixelsPerColumn];
|
||||
|
||||
for (ushort tileX = 0; tileX < TilesPerRow; tileX++)
|
||||
for (ushort tileY = 0; tileY < TilesPerColumn; tileY++)
|
||||
{
|
||||
var tile = new TilePosition(tileX, tileY);
|
||||
if (map[tileX + tileY * TilesPerRow] != '#')
|
||||
continue;
|
||||
|
||||
for (byte pixelInTileX = 0; pixelInTileX < TileSize; pixelInTileX++)
|
||||
for (byte pixelInTileY = 0; pixelInTileY < TileSize; pixelInTileY++)
|
||||
{
|
||||
var (x, y) = tile.ToPixelPosition().GetPixelRelative(pixelInTileX, pixelInTileY);
|
||||
walls[x, y] = true;
|
||||
}
|
||||
}
|
||||
|
||||
_maps.Add(Path.GetFileName(file), walls);
|
||||
}
|
||||
|
||||
public bool TrySwitchTo(string name)
|
||||
{
|
||||
if (!_maps.TryGetValue(name, out var mapData))
|
||||
return false;
|
||||
Current = new Map(name, (bool[,]) mapData.Clone());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Map(string name, bool[,] walls)
|
||||
{
|
||||
public string Name => name;
|
||||
|
||||
public bool IsWall(int x, int y) => walls[x, y];
|
||||
|
||||
public bool IsWall(PixelPosition position) => walls[position.X, position.Y];
|
||||
|
||||
public bool IsWall(TilePosition position)
|
||||
{
|
||||
var pixel = position.ToPixelPosition();
|
||||
|
||||
for (short dx = 1; dx < MapService.TilesPerRow - 1; dx++)
|
||||
for (short dy = 1; dy < MapService.TilesPerColumn - 1; dy++)
|
||||
{
|
||||
if (IsWall(pixel.GetPixelRelative(dx, dy)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void DestroyWallAt(PixelPosition pixel) => walls[pixel.X, pixel.Y] = false;
|
||||
}
|
25
tanks-backend/TanksServer/GameLogic/MoveBullets.cs
Normal file
25
tanks-backend/TanksServer/GameLogic/MoveBullets.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class MoveBullets(
|
||||
MapEntityManager entityManager,
|
||||
IOptions<GameRules> config
|
||||
) : ITickStep
|
||||
{
|
||||
public Task TickAsync(TimeSpan delta)
|
||||
{
|
||||
foreach (var bullet in entityManager.Bullets)
|
||||
MoveBullet(bullet, delta);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void MoveBullet(Bullet bullet, TimeSpan delta)
|
||||
{
|
||||
var speed = config.Value.BulletSpeed * delta.TotalSeconds;
|
||||
var angle = bullet.Rotation * 2 * Math.PI;
|
||||
bullet.Position = new FloatPosition(
|
||||
bullet.Position.X + Math.Sin(angle) * speed,
|
||||
bullet.Position.Y - Math.Cos(angle) * speed
|
||||
);
|
||||
}
|
||||
}
|
80
tanks-backend/TanksServer/GameLogic/MoveTanks.cs
Normal file
80
tanks-backend/TanksServer/GameLogic/MoveTanks.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class MoveTanks(
|
||||
MapEntityManager entityManager,
|
||||
IOptions<GameRules> options,
|
||||
MapService map
|
||||
) : ITickStep
|
||||
{
|
||||
private readonly GameRules _config = options.Value;
|
||||
|
||||
public Task TickAsync(TimeSpan delta)
|
||||
{
|
||||
foreach (var tank in entityManager.Tanks)
|
||||
tank.Moved = TryMoveTank(tank, delta);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool TryMoveTank(Tank tank, TimeSpan delta)
|
||||
{
|
||||
var player = tank.Owner;
|
||||
|
||||
double speed;
|
||||
switch (player.Controls)
|
||||
{
|
||||
case { Forward: false, Backward: false }:
|
||||
case { Forward: true, Backward: true }:
|
||||
return false;
|
||||
case { Forward: true }:
|
||||
speed = +_config.MoveSpeed;
|
||||
break;
|
||||
case { Backward: true }:
|
||||
speed = -_config.MoveSpeed;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
speed *= delta.TotalSeconds;
|
||||
|
||||
var angle = tank.Orientation / 16d * 2d * Math.PI;
|
||||
var newX = tank.Position.X + Math.Sin(angle) * speed;
|
||||
var newY = tank.Position.Y - Math.Cos(angle) * speed;
|
||||
|
||||
return TryMoveTankTo(tank, new FloatPosition(newX, newY))
|
||||
|| TryMoveTankTo(tank, new FloatPosition(newX, tank.Position.Y))
|
||||
|| TryMoveTankTo(tank, new FloatPosition(tank.Position.X, newY));
|
||||
}
|
||||
|
||||
private bool TryMoveTankTo(Tank tank, FloatPosition newPosition)
|
||||
{
|
||||
if (HitsWall(newPosition))
|
||||
return false;
|
||||
if (HitsTank(tank, newPosition))
|
||||
return false;
|
||||
|
||||
tank.Position = newPosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HitsTank(Tank tank, FloatPosition newPosition) =>
|
||||
entityManager.Tanks
|
||||
.Where(otherTank => otherTank != tank)
|
||||
.Any(otherTank => newPosition.Distance(otherTank.Position) < MapService.TileSize);
|
||||
|
||||
private bool HitsWall(FloatPosition newPosition)
|
||||
{
|
||||
var (topLeft, _) = newPosition.GetBoundsForCenter(MapService.TileSize);
|
||||
|
||||
for (short y = 0; y < MapService.TileSize; y++)
|
||||
for (short x = 0; x < MapService.TileSize; x++)
|
||||
{
|
||||
var pixelToCheck = topLeft.GetPixelRelative(x, y);
|
||||
if (map.Current.IsWall(pixelToCheck))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
35
tanks-backend/TanksServer/GameLogic/RotateTanks.cs
Normal file
35
tanks-backend/TanksServer/GameLogic/RotateTanks.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class RotateTanks(
|
||||
MapEntityManager entityManager,
|
||||
IOptions<GameRules> options,
|
||||
ILogger<RotateTanks> logger
|
||||
) : ITickStep
|
||||
{
|
||||
private readonly GameRules _config = options.Value;
|
||||
|
||||
public Task TickAsync(TimeSpan delta)
|
||||
{
|
||||
foreach (var tank in entityManager.Tanks)
|
||||
{
|
||||
var player = tank.Owner;
|
||||
|
||||
switch (player.Controls)
|
||||
{
|
||||
case { TurnRight: true, TurnLeft: true }:
|
||||
case { TurnRight: false, TurnLeft: false }:
|
||||
continue;
|
||||
case { TurnLeft: true }:
|
||||
tank.Rotation -= _config.TurnSpeed * delta.TotalSeconds;
|
||||
break;
|
||||
case { TurnRight: true }:
|
||||
tank.Rotation += _config.TurnSpeed * delta.TotalSeconds;
|
||||
break;
|
||||
}
|
||||
|
||||
logger.LogTrace("rotated tank to {}", tank.Rotation);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
58
tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs
Normal file
58
tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class ShootFromTanks(
|
||||
IOptions<GameRules> options,
|
||||
MapEntityManager entityManager
|
||||
) : ITickStep
|
||||
{
|
||||
private readonly GameRules _config = options.Value;
|
||||
|
||||
public Task TickAsync(TimeSpan _)
|
||||
{
|
||||
foreach (var tank in entityManager.Tanks.Where(t => !t.Moved))
|
||||
Shoot(tank);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Shoot(Tank tank)
|
||||
{
|
||||
if (!tank.Owner.Controls.Shoot)
|
||||
return;
|
||||
if (tank.NextShotAfter >= DateTime.Now)
|
||||
return;
|
||||
|
||||
tank.NextShotAfter = DateTime.Now.AddMilliseconds(_config.ShootDelayMs);
|
||||
|
||||
var rotation = tank.Orientation / 16d;
|
||||
var angle = rotation * 2d * Math.PI;
|
||||
|
||||
/* When standing next to a wall, the bullet sometimes misses the first pixel.
|
||||
Spawning the bullet to close to the tank instead means the tank instantly hits itself.
|
||||
Because the tank has a float position, but hit boxes are based on pixels, this problem has been deemed complex
|
||||
enough to do later. These values mostly work. */
|
||||
var distance = (tank.Orientation % 4) switch
|
||||
{
|
||||
0 => 4.4d,
|
||||
1 or 3 => 5.4d,
|
||||
2 => 6d,
|
||||
_ => throw new UnreachableException("this should not be possible")
|
||||
};
|
||||
|
||||
var position = new FloatPosition(
|
||||
tank.Position.X + Math.Sin(angle) * distance,
|
||||
tank.Position.Y - Math.Cos(angle) * distance
|
||||
);
|
||||
|
||||
var explosive = false;
|
||||
if (tank.ExplosiveBullets > 0)
|
||||
{
|
||||
tank.ExplosiveBullets--;
|
||||
explosive = true;
|
||||
}
|
||||
|
||||
entityManager.SpawnBullet(tank.Owner, position, rotation, explosive);
|
||||
}
|
||||
}
|
21
tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs
Normal file
21
tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class SpawnPowerUp(
|
||||
IOptions<GameRules> options,
|
||||
MapEntityManager entityManager
|
||||
) : ITickStep
|
||||
{
|
||||
private readonly double _spawnChance = options.Value.PowerUpSpawnChance;
|
||||
private readonly int _maxCount = options.Value.MaxPowerUpCount;
|
||||
|
||||
public Task TickAsync(TimeSpan delta)
|
||||
{
|
||||
if (entityManager.PowerUps.Count() >= _maxCount)
|
||||
return Task.CompletedTask;
|
||||
if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds)
|
||||
return Task.CompletedTask;
|
||||
|
||||
entityManager.SpawnPowerUp();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
54
tanks-backend/TanksServer/GameLogic/TankSpawnQueue.cs
Normal file
54
tanks-backend/TanksServer/GameLogic/TankSpawnQueue.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class TankSpawnQueue(
|
||||
IOptions<GameRules> options,
|
||||
MapEntityManager entityManager
|
||||
): ITickStep
|
||||
{
|
||||
private readonly ConcurrentQueue<Player> _queue = new();
|
||||
private readonly ConcurrentDictionary<Player, DateTime> _spawnTimes = new();
|
||||
private readonly TimeSpan _spawnDelay = TimeSpan.FromMilliseconds(options.Value.SpawnDelayMs);
|
||||
private readonly TimeSpan _idleTimeout = TimeSpan.FromMilliseconds(options.Value.IdleTimeoutMs);
|
||||
|
||||
public void EnqueueForImmediateSpawn(Player player) => _queue.Enqueue(player);
|
||||
|
||||
public void EnqueueForDelayedSpawn(Player player)
|
||||
{
|
||||
_queue.Enqueue(player);
|
||||
_spawnTimes.AddOrUpdate(player, DateTime.MinValue, (_, _) => DateTime.Now + _spawnDelay);
|
||||
}
|
||||
|
||||
private bool TryDequeueNext([MaybeNullWhen(false)] out Player player)
|
||||
{
|
||||
if (!_queue.TryDequeue(out player))
|
||||
return false; // no one on queue
|
||||
|
||||
if (player.LastInput + _idleTimeout < DateTime.Now)
|
||||
{
|
||||
// player idle
|
||||
_queue.Enqueue(player);
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
if (_spawnTimes.GetOrAdd(player, DateTime.MinValue) > now)
|
||||
{
|
||||
// spawn delay
|
||||
_queue.Enqueue(player);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task TickAsync(TimeSpan _)
|
||||
{
|
||||
if (!TryDequeueNext(out var player))
|
||||
return Task.CompletedTask;
|
||||
|
||||
entityManager.SpawnTank(player);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue