add explosive bullet power up
This commit is contained in:
parent
3f4a301993
commit
a2d46bda92
|
@ -104,3 +104,8 @@ There are other commands implemented as well, e.g. for changing the brightness.
|
||||||
- 10: bullet
|
- 10: bullet
|
||||||
- 11: (reserved)
|
- 11: (reserved)
|
||||||
- client responds with empty message to request the next frame
|
- client responds with empty message to request the next frame
|
||||||
|
|
||||||
|
## Backlog: Bugs, Wishes, Ideas
|
||||||
|
- Generalize drawing of entities as there are multiple classes with pretty much the same code
|
||||||
|
- Generalize hit box collision
|
||||||
|
- BUG: when standing next to a wall, the bullet sometimes misses the first pixel
|
||||||
|
|
76
TanksServer/Endpoints.cs
Normal file
76
TanksServer/Endpoints.cs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using TanksServer.GameLogic;
|
||||||
|
using TanksServer.Interactivity;
|
||||||
|
|
||||||
|
namespace TanksServer;
|
||||||
|
|
||||||
|
internal static class Endpoints
|
||||||
|
{
|
||||||
|
public static void MapEndpoints(WebApplication app)
|
||||||
|
{
|
||||||
|
var clientScreenServer = app.Services.GetRequiredService<ClientScreenServer>();
|
||||||
|
var playerService = app.Services.GetRequiredService<PlayerServer>();
|
||||||
|
var controlsServer = app.Services.GetRequiredService<ControlsServer>();
|
||||||
|
var mapService = app.Services.GetRequiredService<MapService>();
|
||||||
|
|
||||||
|
app.MapPost("/player", (string name, Guid? id) =>
|
||||||
|
{
|
||||||
|
name = name.Trim().ToUpperInvariant();
|
||||||
|
if (name == string.Empty)
|
||||||
|
return Results.BadRequest("name cannot be blank");
|
||||||
|
if (name.Length > 12)
|
||||||
|
return Results.BadRequest("name too long");
|
||||||
|
|
||||||
|
var player = playerService.GetOrAdd(name, id ?? Guid.NewGuid());
|
||||||
|
return player != null
|
||||||
|
? Results.Ok(new NameId(player.Name, player.Id))
|
||||||
|
: Results.Unauthorized();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/player", ([FromQuery] Guid id) =>
|
||||||
|
playerService.TryGet(id, out var foundPlayer)
|
||||||
|
? Results.Ok((object?)foundPlayer)
|
||||||
|
: Results.NotFound()
|
||||||
|
);
|
||||||
|
|
||||||
|
app.MapGet("/scores", () => playerService.GetAll());
|
||||||
|
|
||||||
|
app.Map("/screen", async (HttpContext context, [FromQuery] Guid? player) =>
|
||||||
|
{
|
||||||
|
if (!context.WebSockets.IsWebSocketRequest)
|
||||||
|
return Results.BadRequest();
|
||||||
|
|
||||||
|
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
await clientScreenServer.HandleClient(ws, player);
|
||||||
|
return Results.Empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.Map("/controls", async (HttpContext context, [FromQuery] Guid playerId) =>
|
||||||
|
{
|
||||||
|
if (!context.WebSockets.IsWebSocketRequest)
|
||||||
|
return Results.BadRequest();
|
||||||
|
|
||||||
|
if (!playerService.TryGet(playerId, out var player))
|
||||||
|
return Results.NotFound();
|
||||||
|
|
||||||
|
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
await controlsServer.HandleClient(ws, player);
|
||||||
|
return Results.Empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapGet("/map", () => mapService.MapNames);
|
||||||
|
|
||||||
|
app.MapPost("/map", ([FromQuery] string name) =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
return Results.BadRequest("invalid map name");
|
||||||
|
if (!mapService.TrySwitchTo(name))
|
||||||
|
return Results.NotFound("map with name not found");
|
||||||
|
return Results.Ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
namespace TanksServer.GameLogic;
|
||||||
|
|
||||||
public class GameRulesConfiguration
|
internal sealed class GameRules
|
||||||
{
|
{
|
||||||
public bool DestructibleWalls { get; set; } = true;
|
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;
|
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)
|
public Task TickAsync(TimeSpan delta)
|
||||||
{
|
{
|
||||||
foreach (var bullet in bullets.GetAll())
|
foreach (var bullet in entityManager.Bullets)
|
||||||
MoveBullet(bullet, delta);
|
MoveBullet(bullet, delta);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
namespace TanksServer.GameLogic;
|
namespace TanksServer.GameLogic;
|
||||||
|
|
||||||
internal sealed class MoveTanks(
|
internal sealed class MoveTanks(
|
||||||
TankManager tanks,
|
MapEntityManager entityManager,
|
||||||
IOptions<TanksConfiguration> options,
|
IOptions<TanksConfiguration> options,
|
||||||
MapService map
|
MapService map
|
||||||
) : ITickStep
|
) : ITickStep
|
||||||
|
@ -10,7 +10,7 @@ internal sealed class MoveTanks(
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan delta)
|
public Task TickAsync(TimeSpan delta)
|
||||||
{
|
{
|
||||||
foreach (var tank in tanks)
|
foreach (var tank in entityManager.Tanks)
|
||||||
tank.Moved = TryMoveTank(tank, delta);
|
tank.Moved = TryMoveTank(tank, delta);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -59,13 +59,13 @@ internal sealed class MoveTanks(
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HitsTank(Tank tank, FloatPosition newPosition) =>
|
private bool HitsTank(Tank tank, FloatPosition newPosition) =>
|
||||||
tanks
|
entityManager.Tanks
|
||||||
.Where(otherTank => otherTank != tank)
|
.Where(otherTank => otherTank != tank)
|
||||||
.Any(otherTank => newPosition.Distance(otherTank.Position) < MapService.TileSize);
|
.Any(otherTank => newPosition.Distance(otherTank.Position) < MapService.TileSize);
|
||||||
|
|
||||||
private bool HitsWall(FloatPosition newPosition)
|
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 y = 0; y < MapService.TileSize; y++)
|
||||||
for (short x = 0; x < MapService.TileSize; x++)
|
for (short x = 0; x < MapService.TileSize; x++)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
namespace TanksServer.GameLogic;
|
namespace TanksServer.GameLogic;
|
||||||
|
|
||||||
internal sealed class RotateTanks(
|
internal sealed class RotateTanks(
|
||||||
TankManager tanks,
|
MapEntityManager entityManager,
|
||||||
IOptions<TanksConfiguration> options,
|
IOptions<TanksConfiguration> options,
|
||||||
ILogger<RotateTanks> logger
|
ILogger<RotateTanks> logger
|
||||||
) : ITickStep
|
) : ITickStep
|
||||||
|
@ -10,7 +10,7 @@ internal sealed class RotateTanks(
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan delta)
|
public Task TickAsync(TimeSpan delta)
|
||||||
{
|
{
|
||||||
foreach (var tank in tanks)
|
foreach (var tank in entityManager.Tanks)
|
||||||
{
|
{
|
||||||
var player = tank.Owner;
|
var player = tank.Owner;
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,15 @@ using System.Diagnostics;
|
||||||
namespace TanksServer.GameLogic;
|
namespace TanksServer.GameLogic;
|
||||||
|
|
||||||
internal sealed class ShootFromTanks(
|
internal sealed class ShootFromTanks(
|
||||||
TankManager tanks,
|
|
||||||
IOptions<TanksConfiguration> options,
|
IOptions<TanksConfiguration> options,
|
||||||
BulletManager bulletManager
|
MapEntityManager entityManager
|
||||||
) : ITickStep
|
) : ITickStep
|
||||||
{
|
{
|
||||||
private readonly TanksConfiguration _config = options.Value;
|
private readonly TanksConfiguration _config = options.Value;
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan _)
|
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);
|
Shoot(tank);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -30,7 +29,7 @@ internal sealed class ShootFromTanks(
|
||||||
var rotation = tank.Orientation / 16d;
|
var rotation = tank.Orientation / 16d;
|
||||||
var angle = rotation * 2d * Math.PI;
|
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.
|
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
|
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. */
|
enough to do later. These values mostly work. */
|
||||||
|
@ -47,6 +46,13 @@ internal sealed class ShootFromTanks(
|
||||||
tank.Position.Y - Math.Cos(angle) * distance
|
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;
|
namespace TanksServer.GameLogic;
|
||||||
|
|
||||||
internal sealed class SpawnQueue(
|
internal sealed class TankSpawnQueue(
|
||||||
IOptions<PlayersConfiguration> options
|
IOptions<PlayersConfiguration> options,
|
||||||
)
|
MapEntityManager entityManager
|
||||||
|
): ITickStep
|
||||||
{
|
{
|
||||||
private readonly ConcurrentQueue<Player> _queue = new();
|
private readonly ConcurrentQueue<Player> _queue = new();
|
||||||
private readonly ConcurrentDictionary<Player, DateTime> _spawnTimes = new();
|
private readonly ConcurrentDictionary<Player, DateTime> _spawnTimes = new();
|
||||||
|
@ -41,4 +42,13 @@ internal sealed class SpawnQueue(
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task TickAsync(TimeSpan _)
|
||||||
|
{
|
||||||
|
if (!TryDequeueNext(out var player))
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
entityManager.SpawnTank(player);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,11 +2,11 @@ using TanksServer.GameLogic;
|
||||||
|
|
||||||
namespace TanksServer.Graphics;
|
namespace TanksServer.Graphics;
|
||||||
|
|
||||||
internal sealed class DrawBulletsStep(BulletManager bullets) : IDrawStep
|
internal sealed class DrawBulletsStep(MapEntityManager entityManager) : IDrawStep
|
||||||
{
|
{
|
||||||
public void Draw(GamePixelGrid pixels)
|
public void Draw(GamePixelGrid pixels)
|
||||||
{
|
{
|
||||||
foreach (var bullet in bullets.GetAll())
|
foreach (var bullet in entityManager.Bullets)
|
||||||
{
|
{
|
||||||
var position = bullet.Position.ToPixelPosition();
|
var position = bullet.Position.ToPixelPosition();
|
||||||
pixels[position.X, position.Y].EntityType = GamePixelEntityType.Bullet;
|
pixels[position.X, position.Y].EntityType = GamePixelEntityType.Bullet;
|
||||||
|
|
52
TanksServer/Graphics/DrawPowerUpsStep.cs
Normal file
52
TanksServer/Graphics/DrawPowerUpsStep.cs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using TanksServer.GameLogic;
|
||||||
|
|
||||||
|
namespace TanksServer.Graphics;
|
||||||
|
|
||||||
|
internal sealed class DrawPowerUpsStep : IDrawStep
|
||||||
|
{
|
||||||
|
private readonly MapEntityManager _entityManager;
|
||||||
|
private readonly bool?[,] _explosiveSprite;
|
||||||
|
|
||||||
|
public DrawPowerUpsStep(MapEntityManager entityManager)
|
||||||
|
{
|
||||||
|
_entityManager = entityManager;
|
||||||
|
|
||||||
|
using var tankImage = Image.Load<Rgba32>("assets/powerup_explosive.png");
|
||||||
|
Debug.Assert(tankImage.Width == tankImage.Height && tankImage.Width == MapService.TileSize);
|
||||||
|
_explosiveSprite = new bool?[tankImage.Width, tankImage.Height];
|
||||||
|
|
||||||
|
var whitePixel = new Rgba32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
|
||||||
|
for (var y = 0; y < tankImage.Height; y++)
|
||||||
|
for (var x = 0; x < tankImage.Width; x++)
|
||||||
|
{
|
||||||
|
var pixelValue = tankImage[x, y];
|
||||||
|
_explosiveSprite[x, y] = pixelValue.A == 0
|
||||||
|
? null
|
||||||
|
: pixelValue == whitePixel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw(GamePixelGrid pixels)
|
||||||
|
{
|
||||||
|
foreach (var powerUp in _entityManager.PowerUps)
|
||||||
|
{
|
||||||
|
var position = powerUp.Bounds.TopLeft;
|
||||||
|
|
||||||
|
for (byte dy = 0; dy < MapService.TileSize; dy++)
|
||||||
|
for (byte dx = 0; dx < MapService.TileSize; dx++)
|
||||||
|
{
|
||||||
|
var pixelState = _explosiveSprite[dx, dy];
|
||||||
|
if (!pixelState.HasValue)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var (x, y) = position.GetPixelRelative(dx, dy);
|
||||||
|
pixels[x, y].EntityType = pixelState.Value
|
||||||
|
? GamePixelEntityType.PowerUp
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,13 @@ namespace TanksServer.Graphics;
|
||||||
|
|
||||||
internal sealed class DrawTanksStep : IDrawStep
|
internal sealed class DrawTanksStep : IDrawStep
|
||||||
{
|
{
|
||||||
private readonly TankManager _tanks;
|
private readonly MapEntityManager _entityManager;
|
||||||
private readonly bool[] _tankSprite;
|
private readonly bool[] _tankSprite;
|
||||||
private readonly int _tankSpriteWidth;
|
private readonly int _tankSpriteWidth;
|
||||||
|
|
||||||
public DrawTanksStep(TankManager tanks)
|
public DrawTanksStep(MapEntityManager entityManager)
|
||||||
{
|
{
|
||||||
_tanks = tanks;
|
_entityManager = entityManager;
|
||||||
|
|
||||||
using var tankImage = Image.Load<Rgba32>("assets/tank.png");
|
using var tankImage = Image.Load<Rgba32>("assets/tank.png");
|
||||||
_tankSprite = new bool[tankImage.Height * tankImage.Width];
|
_tankSprite = new bool[tankImage.Height * tankImage.Width];
|
||||||
|
@ -28,7 +28,7 @@ internal sealed class DrawTanksStep : IDrawStep
|
||||||
|
|
||||||
public void Draw(GamePixelGrid pixels)
|
public void Draw(GamePixelGrid pixels)
|
||||||
{
|
{
|
||||||
foreach (var tank in _tanks)
|
foreach (var tank in _entityManager.Tanks)
|
||||||
{
|
{
|
||||||
var tankPosition = tank.Bounds.TopLeft;
|
var tankPosition = tank.Bounds.TopLeft;
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,6 @@ internal enum GamePixelEntityType : byte
|
||||||
{
|
{
|
||||||
Wall = 0x0,
|
Wall = 0x0,
|
||||||
Tank = 0x1,
|
Tank = 0x1,
|
||||||
Bullet = 0x2
|
Bullet = 0x2,
|
||||||
|
PowerUp = 0x3
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ internal sealed class PlayerScreenData(ILogger logger)
|
||||||
{
|
{
|
||||||
var result = (byte)(isCurrentPlayer ? 0x1 : 0x0);
|
var result = (byte)(isCurrentPlayer ? 0x1 : 0x0);
|
||||||
var kind = (byte)entityKind;
|
var kind = (byte)entityKind;
|
||||||
Debug.Assert(kind < 3);
|
Debug.Assert(kind <= 3);
|
||||||
result += (byte)(kind << 2);
|
result += (byte)(kind << 2);
|
||||||
|
|
||||||
var index = _count / 2;
|
var index = _count / 2;
|
||||||
|
|
|
@ -3,7 +3,7 @@ using TanksServer.GameLogic;
|
||||||
|
|
||||||
namespace TanksServer.Interactivity;
|
namespace TanksServer.Interactivity;
|
||||||
|
|
||||||
internal sealed class PlayerServer(ILogger<PlayerServer> logger, SpawnQueue spawnQueue)
|
internal sealed class PlayerServer(ILogger<PlayerServer> logger, TankSpawnQueue tankSpawnQueue)
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, Player> _players = new();
|
private readonly ConcurrentDictionary<string, Player> _players = new();
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ internal sealed class PlayerServer(ILogger<PlayerServer> logger, SpawnQueue spaw
|
||||||
Player AddAndSpawn()
|
Player AddAndSpawn()
|
||||||
{
|
{
|
||||||
var player = new Player(name, id);
|
var player = new Player(name, id);
|
||||||
spawnQueue.EnqueueForImmediateSpawn(player);
|
tankSpawnQueue.EnqueueForImmediateSpawn(player);
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
namespace TanksServer.Models;
|
namespace TanksServer.Models;
|
||||||
|
|
||||||
internal sealed class Bullet(Player tankOwner, FloatPosition position, double rotation) : IMapEntity
|
internal sealed class Bullet(Player tankOwner, FloatPosition position, double rotation, bool isExplosive) : IMapEntity
|
||||||
{
|
{
|
||||||
public Player Owner { get; } = tankOwner;
|
public Player Owner { get; } = tankOwner;
|
||||||
|
|
||||||
public double Rotation { get; set; } = rotation;
|
public double Rotation { get; } = rotation;
|
||||||
|
|
||||||
public FloatPosition Position { get; set; } = position;
|
public FloatPosition Position { get; set; } = position;
|
||||||
|
|
||||||
|
public bool IsExplosive { get; } = isExplosive;
|
||||||
|
|
||||||
public PixelBounds Bounds => new (Position.ToPixelPosition(), Position.ToPixelPosition());
|
public PixelBounds Bounds => new (Position.ToPixelPosition(), Position.ToPixelPosition());
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,4 +28,15 @@ internal static class PositionHelpers
|
||||||
Math.Pow(p1.X - p2.X, 2) +
|
Math.Pow(p1.X - p2.X, 2) +
|
||||||
Math.Pow(p1.Y - p2.Y, 2)
|
Math.Pow(p1.Y - p2.Y, 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static PixelBounds GetBoundsForCenter(this FloatPosition position, ushort size)
|
||||||
|
{
|
||||||
|
var sub = (short)(-size / 2d);
|
||||||
|
var add = (short)(size / 2d - 1);
|
||||||
|
var pixelPosition = position.ToPixelPosition();
|
||||||
|
return new PixelBounds(
|
||||||
|
pixelPosition.GetPixelRelative(sub, sub),
|
||||||
|
pixelPosition.GetPixelRelative(add, add)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
10
TanksServer/Models/PowerUp.cs
Normal file
10
TanksServer/Models/PowerUp.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using TanksServer.GameLogic;
|
||||||
|
|
||||||
|
namespace TanksServer.Models;
|
||||||
|
|
||||||
|
internal sealed class PowerUp(FloatPosition position): IMapEntity
|
||||||
|
{
|
||||||
|
public FloatPosition Position { get; set; } = position;
|
||||||
|
|
||||||
|
public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize);
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using TanksServer.GameLogic;
|
||||||
|
|
||||||
namespace TanksServer.Models;
|
namespace TanksServer.Models;
|
||||||
|
|
||||||
|
@ -25,16 +26,9 @@ internal sealed class Tank(Player player, FloatPosition spawnPosition) : IMapEnt
|
||||||
|
|
||||||
public FloatPosition Position { get; set; } = spawnPosition;
|
public FloatPosition Position { get; set; } = spawnPosition;
|
||||||
|
|
||||||
public PixelBounds Bounds => GetBoundsForCenter(Position);
|
public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize);
|
||||||
|
|
||||||
public int Orientation => (int)Math.Round(Rotation * 16) % 16;
|
public int Orientation => (int)Math.Round(Rotation * 16) % 16;
|
||||||
|
|
||||||
public static PixelBounds GetBoundsForCenter(FloatPosition position)
|
public byte ExplosiveBullets { get; set; }
|
||||||
{
|
|
||||||
var pixelPosition = position.ToPixelPosition();
|
|
||||||
return new PixelBounds(
|
|
||||||
pixelPosition.GetPixelRelative(-4, -4),
|
|
||||||
pixelPosition.GetPixelRelative(3, 3)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using DisplayCommands;
|
using DisplayCommands;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
@ -20,70 +18,11 @@ public static class Program
|
||||||
{
|
{
|
||||||
var app = Configure(args);
|
var app = Configure(args);
|
||||||
|
|
||||||
var clientScreenServer = app.Services.GetRequiredService<ClientScreenServer>();
|
|
||||||
var playerService = app.Services.GetRequiredService<PlayerServer>();
|
|
||||||
var controlsServer = app.Services.GetRequiredService<ControlsServer>();
|
|
||||||
var mapService = app.Services.GetRequiredService<MapService>();
|
|
||||||
|
|
||||||
var clientFileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "client"));
|
var clientFileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "client"));
|
||||||
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider });
|
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider });
|
||||||
app.UseStaticFiles(new StaticFileOptions { FileProvider = clientFileProvider });
|
app.UseStaticFiles(new StaticFileOptions { FileProvider = clientFileProvider });
|
||||||
|
|
||||||
app.MapPost("/player", (string name, Guid? id) =>
|
Endpoints.MapEndpoints(app);
|
||||||
{
|
|
||||||
name = name.Trim().ToUpperInvariant();
|
|
||||||
if (name == string.Empty)
|
|
||||||
return Results.BadRequest("name cannot be blank");
|
|
||||||
if (name.Length > 12)
|
|
||||||
return Results.BadRequest("name too long");
|
|
||||||
|
|
||||||
var player = playerService.GetOrAdd(name, id ?? Guid.NewGuid());
|
|
||||||
return player != null
|
|
||||||
? Results.Ok(new NameId(player.Name, player.Id))
|
|
||||||
: Results.Unauthorized();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.MapGet("/player", ([FromQuery] Guid id) =>
|
|
||||||
playerService.TryGet(id, out var foundPlayer)
|
|
||||||
? Results.Ok((object?)foundPlayer)
|
|
||||||
: Results.NotFound()
|
|
||||||
);
|
|
||||||
|
|
||||||
app.MapGet("/scores", () => playerService.GetAll());
|
|
||||||
|
|
||||||
app.Map("/screen", async (HttpContext context, [FromQuery] Guid? player) =>
|
|
||||||
{
|
|
||||||
if (!context.WebSockets.IsWebSocketRequest)
|
|
||||||
return Results.BadRequest();
|
|
||||||
|
|
||||||
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
|
||||||
await clientScreenServer.HandleClient(ws, player);
|
|
||||||
return Results.Empty;
|
|
||||||
});
|
|
||||||
|
|
||||||
app.Map("/controls", async (HttpContext context, [FromQuery] Guid playerId) =>
|
|
||||||
{
|
|
||||||
if (!context.WebSockets.IsWebSocketRequest)
|
|
||||||
return Results.BadRequest();
|
|
||||||
|
|
||||||
if (!playerService.TryGet(playerId, out var player))
|
|
||||||
return Results.NotFound();
|
|
||||||
|
|
||||||
using var ws = await context.WebSockets.AcceptWebSocketAsync();
|
|
||||||
await controlsServer.HandleClient(ws, player);
|
|
||||||
return Results.Empty;
|
|
||||||
});
|
|
||||||
|
|
||||||
app.MapGet("/map", () => mapService.MapNames);
|
|
||||||
|
|
||||||
app.MapPost("/map", ([FromQuery] string name) =>
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
return Results.BadRequest("invalid map name");
|
|
||||||
if (!mapService.TrySwitchTo(name))
|
|
||||||
return Results.NotFound("map with name not found");
|
|
||||||
return Results.Ok();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
}
|
}
|
||||||
|
@ -119,27 +58,28 @@ public static class Program
|
||||||
throw new InvalidOperationException("'Host' configuration missing");
|
throw new InvalidOperationException("'Host' configuration missing");
|
||||||
|
|
||||||
builder.Services.AddSingleton<MapService>();
|
builder.Services.AddSingleton<MapService>();
|
||||||
builder.Services.AddSingleton<BulletManager>();
|
builder.Services.AddSingleton<MapEntityManager>();
|
||||||
builder.Services.AddSingleton<TankManager>();
|
|
||||||
builder.Services.AddSingleton<ControlsServer>();
|
builder.Services.AddSingleton<ControlsServer>();
|
||||||
builder.Services.AddSingleton<PlayerServer>();
|
builder.Services.AddSingleton<PlayerServer>();
|
||||||
builder.Services.AddSingleton<ClientScreenServer>();
|
builder.Services.AddSingleton<ClientScreenServer>();
|
||||||
builder.Services.AddSingleton<SpawnQueue>();
|
builder.Services.AddSingleton<TankSpawnQueue>();
|
||||||
|
|
||||||
builder.Services.AddHostedService<GameTickWorker>();
|
builder.Services.AddHostedService<GameTickWorker>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ControlsServer>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<ControlsServer>());
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ClientScreenServer>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<ClientScreenServer>());
|
||||||
|
|
||||||
builder.Services.AddSingleton<ITickStep, MoveBullets>();
|
builder.Services.AddSingleton<ITickStep, MoveBullets>();
|
||||||
builder.Services.AddSingleton<ITickStep, CollideBulletsWithTanks>();
|
builder.Services.AddSingleton<ITickStep, CollideBullets>();
|
||||||
builder.Services.AddSingleton<ITickStep, CollideBulletsWithMap>();
|
|
||||||
builder.Services.AddSingleton<ITickStep, RotateTanks>();
|
builder.Services.AddSingleton<ITickStep, RotateTanks>();
|
||||||
builder.Services.AddSingleton<ITickStep, MoveTanks>();
|
builder.Services.AddSingleton<ITickStep, MoveTanks>();
|
||||||
builder.Services.AddSingleton<ITickStep, ShootFromTanks>();
|
builder.Services.AddSingleton<ITickStep, ShootFromTanks>();
|
||||||
builder.Services.AddSingleton<ITickStep, SpawnNewTanks>();
|
builder.Services.AddSingleton<ITickStep, CollectPowerUp>();
|
||||||
|
builder.Services.AddSingleton<ITickStep>(sp => sp.GetRequiredService<TankSpawnQueue>());
|
||||||
|
builder.Services.AddSingleton<ITickStep, SpawnPowerUp>();
|
||||||
builder.Services.AddSingleton<ITickStep, GeneratePixelsTickStep>();
|
builder.Services.AddSingleton<ITickStep, GeneratePixelsTickStep>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IDrawStep, DrawMapStep>();
|
builder.Services.AddSingleton<IDrawStep, DrawMapStep>();
|
||||||
|
builder.Services.AddSingleton<IDrawStep, DrawPowerUpsStep>();
|
||||||
builder.Services.AddSingleton<IDrawStep, DrawTanksStep>();
|
builder.Services.AddSingleton<IDrawStep, DrawTanksStep>();
|
||||||
builder.Services.AddSingleton<IDrawStep, DrawBulletsStep>();
|
builder.Services.AddSingleton<IDrawStep, DrawBulletsStep>();
|
||||||
|
|
||||||
|
@ -150,7 +90,7 @@ public static class Program
|
||||||
builder.Configuration.GetSection("Tanks"));
|
builder.Configuration.GetSection("Tanks"));
|
||||||
builder.Services.Configure<PlayersConfiguration>(
|
builder.Services.Configure<PlayersConfiguration>(
|
||||||
builder.Configuration.GetSection("Players"));
|
builder.Configuration.GetSection("Players"));
|
||||||
builder.Services.Configure<GameRulesConfiguration>(builder.Configuration.GetSection("GameRules"));
|
builder.Services.Configure<GameRules>(builder.Configuration.GetSection("GameRules"));
|
||||||
|
|
||||||
if (hostConfiguration.EnableServicePointDisplay)
|
if (hostConfiguration.EnableServicePointDisplay)
|
||||||
{
|
{
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
"BulletSpeed": 75
|
"BulletSpeed": 75
|
||||||
},
|
},
|
||||||
"GameRules": {
|
"GameRules": {
|
||||||
"DestructibleWalls": true
|
"DestructibleWalls": true,
|
||||||
|
"PowerUpSpawnChance": 0.1
|
||||||
},
|
},
|
||||||
"Players": {
|
"Players": {
|
||||||
"SpawnDelayMs": 3000,
|
"SpawnDelayMs": 3000,
|
||||||
|
|
BIN
TanksServer/assets/powerup_explosive.png
Normal file
BIN
TanksServer/assets/powerup_explosive.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 B |
Loading…
Reference in a new issue