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
|
||||
- 11: (reserved)
|
||||
- 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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@ using TanksServer.GameLogic;
|
|||
|
||||
namespace TanksServer.Graphics;
|
||||
|
||||
internal sealed class DrawBulletsStep(BulletManager bullets) : IDrawStep
|
||||
internal sealed class DrawBulletsStep(MapEntityManager entityManager) : IDrawStep
|
||||
{
|
||||
public void Draw(GamePixelGrid pixels)
|
||||
{
|
||||
foreach (var bullet in bullets.GetAll())
|
||||
foreach (var bullet in entityManager.Bullets)
|
||||
{
|
||||
var position = bullet.Position.ToPixelPosition();
|
||||
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
|
||||
{
|
||||
private readonly TankManager _tanks;
|
||||
private readonly MapEntityManager _entityManager;
|
||||
private readonly bool[] _tankSprite;
|
||||
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");
|
||||
_tankSprite = new bool[tankImage.Height * tankImage.Width];
|
||||
|
@ -28,7 +28,7 @@ internal sealed class DrawTanksStep : IDrawStep
|
|||
|
||||
public void Draw(GamePixelGrid pixels)
|
||||
{
|
||||
foreach (var tank in _tanks)
|
||||
foreach (var tank in _entityManager.Tanks)
|
||||
{
|
||||
var tankPosition = tank.Bounds.TopLeft;
|
||||
|
||||
|
|
|
@ -4,5 +4,6 @@ internal enum GamePixelEntityType : byte
|
|||
{
|
||||
Wall = 0x0,
|
||||
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 kind = (byte)entityKind;
|
||||
Debug.Assert(kind < 3);
|
||||
Debug.Assert(kind <= 3);
|
||||
result += (byte)(kind << 2);
|
||||
|
||||
var index = _count / 2;
|
||||
|
|
|
@ -3,7 +3,7 @@ using TanksServer.GameLogic;
|
|||
|
||||
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();
|
||||
|
||||
|
@ -12,7 +12,7 @@ internal sealed class PlayerServer(ILogger<PlayerServer> logger, SpawnQueue spaw
|
|||
Player AddAndSpawn()
|
||||
{
|
||||
var player = new Player(name, id);
|
||||
spawnQueue.EnqueueForImmediateSpawn(player);
|
||||
tankSpawnQueue.EnqueueForImmediateSpawn(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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 double Rotation { get; set; } = rotation;
|
||||
public double Rotation { get; } = rotation;
|
||||
|
||||
public FloatPosition Position { get; set; } = position;
|
||||
|
||||
public bool IsExplosive { get; } = isExplosive;
|
||||
|
||||
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.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 TanksServer.GameLogic;
|
||||
|
||||
namespace TanksServer.Models;
|
||||
|
||||
|
@ -25,16 +26,9 @@ internal sealed class Tank(Player player, FloatPosition spawnPosition) : IMapEnt
|
|||
|
||||
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 static PixelBounds GetBoundsForCenter(FloatPosition position)
|
||||
{
|
||||
var pixelPosition = position.ToPixelPosition();
|
||||
return new PixelBounds(
|
||||
pixelPosition.GetPixelRelative(-4, -4),
|
||||
pixelPosition.GetPixelRelative(3, 3)
|
||||
);
|
||||
}
|
||||
public byte ExplosiveBullets { get; set; }
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
using System.IO;
|
||||
using DisplayCommands;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
|
@ -20,70 +18,11 @@ public static class Program
|
|||
{
|
||||
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"));
|
||||
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider });
|
||||
app.UseStaticFiles(new StaticFileOptions { FileProvider = clientFileProvider });
|
||||
|
||||
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();
|
||||
});
|
||||
Endpoints.MapEndpoints(app);
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
@ -119,27 +58,28 @@ public static class Program
|
|||
throw new InvalidOperationException("'Host' configuration missing");
|
||||
|
||||
builder.Services.AddSingleton<MapService>();
|
||||
builder.Services.AddSingleton<BulletManager>();
|
||||
builder.Services.AddSingleton<TankManager>();
|
||||
builder.Services.AddSingleton<MapEntityManager>();
|
||||
builder.Services.AddSingleton<ControlsServer>();
|
||||
builder.Services.AddSingleton<PlayerServer>();
|
||||
builder.Services.AddSingleton<ClientScreenServer>();
|
||||
builder.Services.AddSingleton<SpawnQueue>();
|
||||
builder.Services.AddSingleton<TankSpawnQueue>();
|
||||
|
||||
builder.Services.AddHostedService<GameTickWorker>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ControlsServer>());
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ClientScreenServer>());
|
||||
|
||||
builder.Services.AddSingleton<ITickStep, MoveBullets>();
|
||||
builder.Services.AddSingleton<ITickStep, CollideBulletsWithTanks>();
|
||||
builder.Services.AddSingleton<ITickStep, CollideBulletsWithMap>();
|
||||
builder.Services.AddSingleton<ITickStep, CollideBullets>();
|
||||
builder.Services.AddSingleton<ITickStep, RotateTanks>();
|
||||
builder.Services.AddSingleton<ITickStep, MoveTanks>();
|
||||
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<IDrawStep, DrawMapStep>();
|
||||
builder.Services.AddSingleton<IDrawStep, DrawPowerUpsStep>();
|
||||
builder.Services.AddSingleton<IDrawStep, DrawTanksStep>();
|
||||
builder.Services.AddSingleton<IDrawStep, DrawBulletsStep>();
|
||||
|
||||
|
@ -150,7 +90,7 @@ public static class Program
|
|||
builder.Configuration.GetSection("Tanks"));
|
||||
builder.Services.Configure<PlayersConfiguration>(
|
||||
builder.Configuration.GetSection("Players"));
|
||||
builder.Services.Configure<GameRulesConfiguration>(builder.Configuration.GetSection("GameRules"));
|
||||
builder.Services.Configure<GameRules>(builder.Configuration.GetSection("GameRules"));
|
||||
|
||||
if (hostConfiguration.EnableServicePointDisplay)
|
||||
{
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
"BulletSpeed": 75
|
||||
},
|
||||
"GameRules": {
|
||||
"DestructibleWalls": true
|
||||
"DestructibleWalls": true,
|
||||
"PowerUpSpawnChance": 0.1
|
||||
},
|
||||
"Players": {
|
||||
"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