add explosive bullet power up
This commit is contained in:
parent
3f4a301993
commit
a2d46bda92
30 changed files with 407 additions and 253 deletions
|
@ -1,13 +0,0 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class BulletManager
|
||||
{
|
||||
private readonly HashSet<Bullet> _bullets = [];
|
||||
|
||||
public void Spawn(Player tankOwner, FloatPosition position, double rotation)
|
||||
=> _bullets.Add(new Bullet(tankOwner, position, rotation));
|
||||
|
||||
public IEnumerable<Bullet> GetAll() => _bullets;
|
||||
|
||||
public void RemoveWhere(Predicate<Bullet> predicate) => _bullets.RemoveWhere(predicate);
|
||||
}
|
30
TanksServer/GameLogic/CollectPowerUp.cs
Normal file
30
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;
|
||||
}
|
||||
}
|
73
TanksServer/GameLogic/CollideBullets.cs
Normal file
73
TanksServer/GameLogic/CollideBullets.cs
Normal file
|
@ -0,0 +1,73 @@
|
|||
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);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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);
|
||||
TryHitTankAt(offsetPixel.ToFloatPosition(), owner);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class CollideBulletsWithMap(
|
||||
BulletManager bullets,
|
||||
MapService map,
|
||||
IOptions<GameRulesConfiguration> options
|
||||
) : ITickStep
|
||||
{
|
||||
public Task TickAsync(TimeSpan _)
|
||||
{
|
||||
bullets.RemoveWhere(TryHitAndDestroyWall);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool TryHitAndDestroyWall(Bullet bullet)
|
||||
{
|
||||
var pixel = bullet.Position.ToPixelPosition();
|
||||
if (!map.Current.IsWall(pixel))
|
||||
return false;
|
||||
|
||||
if (options.Value.DestructibleWalls)
|
||||
map.Current.DestroyWallAt(pixel);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class CollideBulletsWithTanks(
|
||||
BulletManager bullets,
|
||||
TankManager tanks,
|
||||
SpawnQueue spawnQueue
|
||||
) : ITickStep
|
||||
{
|
||||
public Task TickAsync(TimeSpan _)
|
||||
{
|
||||
bullets.RemoveWhere(BulletHitsTank);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool BulletHitsTank(Bullet bullet)
|
||||
{
|
||||
foreach (var tank in tanks)
|
||||
{
|
||||
var (topLeft, bottomRight) = tank.Bounds;
|
||||
if (bullet.Position.X < topLeft.X || bullet.Position.X > bottomRight.X ||
|
||||
bullet.Position.Y < topLeft.Y || bullet.Position.Y > bottomRight.Y)
|
||||
continue;
|
||||
|
||||
if (bullet.Owner != tank.Owner)
|
||||
bullet.Owner.Scores.Kills++;
|
||||
tank.Owner.Scores.Deaths++;
|
||||
|
||||
tanks.Remove(tank);
|
||||
spawnQueue.EnqueueForDelayedSpawn(tank.Owner);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
public class GameRulesConfiguration
|
||||
internal sealed class GameRules
|
||||
{
|
||||
public bool DestructibleWalls { get; set; } = true;
|
||||
|
||||
public double PowerUpSpawnChance { get; set; }
|
||||
}
|
65
TanksServer/GameLogic/MapEntityManager.cs
Normal file
65
TanksServer/GameLogic/MapEntityManager.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class MapEntityManager(
|
||||
ILogger<MapEntityManager> logger,
|
||||
MapService map
|
||||
)
|
||||
{
|
||||
private readonly HashSet<Bullet> _bullets = [];
|
||||
private readonly HashSet<Tank> _tanks = [];
|
||||
private readonly HashSet<PowerUp> _powerUps = [];
|
||||
|
||||
public IEnumerable<Bullet> Bullets => _bullets;
|
||||
public IEnumerable<Tank> Tanks => _tanks;
|
||||
public IEnumerable<PowerUp> PowerUps => _powerUps;
|
||||
|
||||
public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, bool isExplosive)
|
||||
=> _bullets.Add(new Bullet(tankOwner, position, rotation, isExplosive));
|
||||
|
||||
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 = Bullets
|
||||
.Cast<IMapEntity>()
|
||||
.Concat(Tanks)
|
||||
.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();
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class MoveBullets(BulletManager bullets, IOptions<TanksConfiguration> config) : ITickStep
|
||||
internal sealed class MoveBullets(
|
||||
MapEntityManager entityManager,
|
||||
IOptions<TanksConfiguration> config
|
||||
) : ITickStep
|
||||
{
|
||||
public Task TickAsync(TimeSpan delta)
|
||||
{
|
||||
foreach (var bullet in bullets.GetAll())
|
||||
foreach (var bullet in entityManager.Bullets)
|
||||
MoveBullet(bullet, delta);
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class MoveTanks(
|
||||
TankManager tanks,
|
||||
MapEntityManager entityManager,
|
||||
IOptions<TanksConfiguration> options,
|
||||
MapService map
|
||||
) : ITickStep
|
||||
|
@ -10,7 +10,7 @@ internal sealed class MoveTanks(
|
|||
|
||||
public Task TickAsync(TimeSpan delta)
|
||||
{
|
||||
foreach (var tank in tanks)
|
||||
foreach (var tank in entityManager.Tanks)
|
||||
tank.Moved = TryMoveTank(tank, delta);
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
@ -59,13 +59,13 @@ internal sealed class MoveTanks(
|
|||
}
|
||||
|
||||
private bool HitsTank(Tank tank, FloatPosition newPosition) =>
|
||||
tanks
|
||||
entityManager.Tanks
|
||||
.Where(otherTank => otherTank != tank)
|
||||
.Any(otherTank => newPosition.Distance(otherTank.Position) < MapService.TileSize);
|
||||
|
||||
private bool HitsWall(FloatPosition newPosition)
|
||||
{
|
||||
var (topLeft, _) = Tank.GetBoundsForCenter(newPosition);
|
||||
var (topLeft, _) = newPosition.GetBoundsForCenter(MapService.TileSize);
|
||||
|
||||
for (short y = 0; y < MapService.TileSize; y++)
|
||||
for (short x = 0; x < MapService.TileSize; x++)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class RotateTanks(
|
||||
TankManager tanks,
|
||||
MapEntityManager entityManager,
|
||||
IOptions<TanksConfiguration> options,
|
||||
ILogger<RotateTanks> logger
|
||||
) : ITickStep
|
||||
|
@ -10,7 +10,7 @@ internal sealed class RotateTanks(
|
|||
|
||||
public Task TickAsync(TimeSpan delta)
|
||||
{
|
||||
foreach (var tank in tanks)
|
||||
foreach (var tank in entityManager.Tanks)
|
||||
{
|
||||
var player = tank.Owner;
|
||||
|
||||
|
|
|
@ -3,16 +3,15 @@ using System.Diagnostics;
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class ShootFromTanks(
|
||||
TankManager tanks,
|
||||
IOptions<TanksConfiguration> options,
|
||||
BulletManager bulletManager
|
||||
MapEntityManager entityManager
|
||||
) : ITickStep
|
||||
{
|
||||
private readonly TanksConfiguration _config = options.Value;
|
||||
|
||||
public Task TickAsync(TimeSpan _)
|
||||
{
|
||||
foreach (var tank in tanks.Where(t => !t.Moved))
|
||||
foreach (var tank in entityManager.Tanks.Where(t => !t.Moved))
|
||||
Shoot(tank);
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
@ -30,7 +29,7 @@ internal sealed class ShootFromTanks(
|
|||
var rotation = tank.Orientation / 16d;
|
||||
var angle = rotation * 2d * Math.PI;
|
||||
|
||||
/* TODO: when standing next to a wall, the bullet sometimes misses the first pixel.
|
||||
/* 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. */
|
||||
|
@ -47,6 +46,13 @@ internal sealed class ShootFromTanks(
|
|||
tank.Position.Y - Math.Cos(angle) * distance
|
||||
);
|
||||
|
||||
bulletManager.Spawn(tank.Owner, position, rotation);
|
||||
var explosive = false;
|
||||
if (tank.ExplosiveBullets > 0)
|
||||
{
|
||||
tank.ExplosiveBullets--;
|
||||
explosive = true;
|
||||
}
|
||||
|
||||
entityManager.SpawnBullet(tank.Owner, position, rotation, explosive);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class SpawnNewTanks(
|
||||
TankManager tanks,
|
||||
MapService map,
|
||||
SpawnQueue queue,
|
||||
BulletManager bullets
|
||||
) : ITickStep
|
||||
{
|
||||
public Task TickAsync(TimeSpan _)
|
||||
{
|
||||
if (!queue.TryDequeueNext(out var player))
|
||||
return Task.CompletedTask;
|
||||
|
||||
tanks.Add(new Tank(player, ChooseSpawnPosition())
|
||||
{
|
||||
Rotation = Random.Shared.NextDouble()
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private 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 = bullets.GetAll()
|
||||
.Cast<IMapEntity>()
|
||||
.Concat(tanks)
|
||||
.Select(entity => entity.Position.Distance(tilePixelCenter))
|
||||
.Aggregate(double.MaxValue, Math.Min);
|
||||
|
||||
candidates.Add(tile, minDistance);
|
||||
}
|
||||
|
||||
var min = candidates.MaxBy(kvp => kvp.Value).Key;
|
||||
return min.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
|
||||
}
|
||||
}
|
18
TanksServer/GameLogic/SpawnPowerUp.cs
Normal file
18
TanksServer/GameLogic/SpawnPowerUp.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class SpawnPowerUp(
|
||||
IOptions<GameRules> options,
|
||||
MapEntityManager entityManager
|
||||
) : ITickStep
|
||||
{
|
||||
private readonly double _spawnChance = options.Value.PowerUpSpawnChance;
|
||||
|
||||
public Task TickAsync(TimeSpan delta)
|
||||
{
|
||||
if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds)
|
||||
return Task.CompletedTask;
|
||||
|
||||
entityManager.SpawnPowerUp();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
using System.Collections;
|
||||
|
||||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class TankManager(ILogger<TankManager> logger) : IEnumerable<Tank>
|
||||
{
|
||||
private readonly ConcurrentDictionary<Tank, byte> _tanks = new();
|
||||
|
||||
public void Add(Tank tank)
|
||||
{
|
||||
logger.LogInformation("Tank added for player {}", tank.Owner.Id);
|
||||
_tanks.TryAdd(tank, 0);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
public IEnumerator<Tank> GetEnumerator() => _tanks.Keys.GetEnumerator();
|
||||
|
||||
public void Remove(Tank tank)
|
||||
{
|
||||
logger.LogInformation("Tank removed for player {}", tank.Owner.Id);
|
||||
_tanks.Remove(tank, out _);
|
||||
}
|
||||
}
|
|
@ -2,9 +2,10 @@ using System.Diagnostics.CodeAnalysis;
|
|||
|
||||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class SpawnQueue(
|
||||
IOptions<PlayersConfiguration> options
|
||||
)
|
||||
internal sealed class TankSpawnQueue(
|
||||
IOptions<PlayersConfiguration> options,
|
||||
MapEntityManager entityManager
|
||||
): ITickStep
|
||||
{
|
||||
private readonly ConcurrentQueue<Player> _queue = new();
|
||||
private readonly ConcurrentDictionary<Player, DateTime> _spawnTimes = new();
|
||||
|
@ -41,4 +42,13 @@ internal sealed class SpawnQueue(
|
|||
|
||||
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