reposition tanks on map switch, rework map logic
This commit is contained in:
parent
3d65c81b8b
commit
97144ae3b8
|
@ -11,7 +11,8 @@ internal sealed class Endpoints(
|
|||
ClientScreenServer clientScreenServer,
|
||||
PlayerServer playerService,
|
||||
ControlsServer controlsServer,
|
||||
MapService mapService
|
||||
MapService mapService,
|
||||
ChangeToRequestedMap changeToRequestedMap
|
||||
)
|
||||
{
|
||||
public void Map(WebApplication app)
|
||||
|
@ -29,8 +30,9 @@ internal sealed class Endpoints(
|
|||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return TypedResults.BadRequest("invalid map name");
|
||||
if (!mapService.TrySwitchTo(name))
|
||||
if (!mapService.TryGetMapByName(name, out var map))
|
||||
return TypedResults.NotFound("map with name not found");
|
||||
changeToRequestedMap.Request(map);
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
|
|
24
tanks-backend/TanksServer/GameLogic/ChangeToRequestedMap.cs
Normal file
24
tanks-backend/TanksServer/GameLogic/ChangeToRequestedMap.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class ChangeToRequestedMap(
|
||||
MapService mapService,
|
||||
MapEntityManager entityManager,
|
||||
EmptyTileFinder emptyTileFinder
|
||||
) : ITickStep
|
||||
{
|
||||
private MapPrototype? _requestedMap;
|
||||
|
||||
public ValueTask TickAsync(TimeSpan delta)
|
||||
{
|
||||
var changeTo = Interlocked.Exchange(ref _requestedMap, null);
|
||||
if (changeTo == null)
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
mapService.SwitchTo(changeTo);
|
||||
foreach (var t in entityManager.Tanks)
|
||||
t.Position = emptyTileFinder.ChooseEmptyTile().GetCenter().ToFloatPosition();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Request(MapPrototype map) => _requestedMap = map;
|
||||
}
|
32
tanks-backend/TanksServer/GameLogic/EmptyTileFinder.cs
Normal file
32
tanks-backend/TanksServer/GameLogic/EmptyTileFinder.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class EmptyTileFinder(
|
||||
MapEntityManager entityManager,
|
||||
MapService mapService
|
||||
)
|
||||
{
|
||||
public TilePosition ChooseEmptyTile()
|
||||
{
|
||||
var maxMinDistance = 0d;
|
||||
TilePosition spawnTile = default;
|
||||
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 (mapService.Current.IsWall(tile))
|
||||
continue;
|
||||
|
||||
var tilePixelCenter = tile.GetCenter().ToFloatPosition();
|
||||
var minDistance = entityManager.AllEntities
|
||||
.Select(entity => entity.Position.Distance(tilePixelCenter))
|
||||
.Aggregate(double.MaxValue, Math.Min);
|
||||
if (minDistance <= maxMinDistance)
|
||||
continue;
|
||||
|
||||
maxMinDistance = minDistance;
|
||||
spawnTile = tile;
|
||||
}
|
||||
|
||||
return spawnTile;
|
||||
}
|
||||
}
|
32
tanks-backend/TanksServer/GameLogic/Map.cs
Normal file
32
tanks-backend/TanksServer/GameLogic/Map.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
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 = 0; dx < MapService.TileSize; dx++)
|
||||
for (short dy = 0; dy < MapService.TileSize; dy++)
|
||||
{
|
||||
if (IsWall(pixel.GetPixelRelative(dx, dy)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryDestroyWallAt(PixelPosition pixel)
|
||||
{
|
||||
var result = walls[pixel.X, pixel.Y];
|
||||
if (result)
|
||||
walls[pixel.X, pixel.Y] = false;
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ namespace TanksServer.GameLogic;
|
|||
|
||||
internal sealed class MapEntityManager(
|
||||
ILogger<MapEntityManager> logger,
|
||||
MapService map,
|
||||
IOptions<GameRules> options
|
||||
)
|
||||
{
|
||||
|
@ -34,12 +33,12 @@ internal sealed class MapEntityManager(
|
|||
|
||||
public void RemoveWhere(Predicate<Bullet> predicate) => _bullets.RemoveWhere(predicate);
|
||||
|
||||
public void SpawnTank(Player player)
|
||||
public void SpawnTank(Player player, FloatPosition position)
|
||||
{
|
||||
var tank = new Tank
|
||||
{
|
||||
Owner = player,
|
||||
Position = ChooseSpawnPosition(),
|
||||
Position = position,
|
||||
Rotation = Random.Shared.NextDouble(),
|
||||
Magazine = new Magazine(MagazineType.Basic, 0, _rules.MagazineSize)
|
||||
};
|
||||
|
@ -47,12 +46,16 @@ internal sealed class MapEntityManager(
|
|||
logger.LogInformation("Tank added for player {}", player.Name);
|
||||
}
|
||||
|
||||
public void SpawnPowerUp(PowerUpType type, MagazineType? magazineType) => _powerUps.Add(new PowerUp
|
||||
public void SpawnPowerUp(FloatPosition position, PowerUpType type, MagazineType? magazineType)
|
||||
{
|
||||
Position = ChooseSpawnPosition(),
|
||||
Type = type,
|
||||
MagazineType = magazineType
|
||||
});
|
||||
var powerUp = new PowerUp
|
||||
{
|
||||
Position = position,
|
||||
Type = type,
|
||||
MagazineType = magazineType
|
||||
};
|
||||
_powerUps.Add(powerUp);
|
||||
}
|
||||
|
||||
public void RemoveWhere(Predicate<PowerUp> predicate) => _powerUps.RemoveWhere(predicate);
|
||||
|
||||
|
@ -64,33 +67,8 @@ internal sealed class MapEntityManager(
|
|||
|
||||
public Tank? GetCurrentTankOfPlayer(Player player) => _playerTanks.GetValueOrDefault(player);
|
||||
|
||||
private IEnumerable<IMapEntity> AllEntities => Bullets
|
||||
public IEnumerable<IMapEntity> AllEntities => Bullets
|
||||
.Cast<IMapEntity>()
|
||||
.Concat(Tanks)
|
||||
.Concat(PowerUps);
|
||||
|
||||
private FloatPosition ChooseSpawnPosition()
|
||||
{
|
||||
var maxMinDistance = 0d;
|
||||
TilePosition spawnTile = default;
|
||||
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);
|
||||
if (minDistance <= maxMinDistance)
|
||||
continue;
|
||||
|
||||
maxMinDistance = minDistance;
|
||||
spawnTile = tile;
|
||||
}
|
||||
|
||||
return spawnTile.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using TanksServer.Graphics;
|
||||
|
||||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal abstract class MapPrototype
|
||||
{
|
||||
public abstract Map CreateInstance();
|
||||
}
|
||||
|
||||
internal sealed class MapService
|
||||
{
|
||||
public const ushort TilesPerRow = 44;
|
||||
|
@ -12,7 +17,7 @@ internal sealed class MapService
|
|||
public const ushort PixelsPerRow = TilesPerRow * TileSize;
|
||||
public const ushort PixelsPerColumn = TilesPerColumn * TileSize;
|
||||
|
||||
private readonly Dictionary<string, bool[,]> _maps = new();
|
||||
private readonly Dictionary<string, MapPrototype> _maps = new();
|
||||
|
||||
public IEnumerable<string> MapNames => _maps.Keys;
|
||||
|
||||
|
@ -27,88 +32,26 @@ internal sealed class MapService
|
|||
|
||||
var chosenMapIndex = Random.Shared.Next(_maps.Count);
|
||||
var chosenMapName = _maps.Keys.Skip(chosenMapIndex).First();
|
||||
Current = new Map(chosenMapName, _maps[chosenMapName]);
|
||||
Current = _maps[chosenMapName].CreateInstance();
|
||||
}
|
||||
|
||||
public bool TryGetMapByName(string name, [MaybeNullWhen(false)] out MapPrototype map)
|
||||
=> _maps.TryGetValue(name, out map);
|
||||
|
||||
public void SwitchTo(MapPrototype prototype) => Current = prototype.CreateInstance();
|
||||
|
||||
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);
|
||||
var name = Path.GetFileName(file);
|
||||
var prototype = new SpriteMapPrototype(name, Sprite.FromImageFile(file));
|
||||
_maps.Add(Path.GetFileName(file), prototype);
|
||||
}
|
||||
|
||||
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 = 0; dx < MapService.TileSize; dx++)
|
||||
for (short dy = 0; dy < MapService.TileSize; dy++)
|
||||
{
|
||||
if (IsWall(pixel.GetPixelRelative(dx, dy)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryDestroyWallAt(PixelPosition pixel)
|
||||
{
|
||||
var result = walls[pixel.X, pixel.Y];
|
||||
if (result)
|
||||
walls[pixel.X, pixel.Y] = false;
|
||||
return result;
|
||||
var name = Path.GetFileName(file);
|
||||
var prototype = new TextMapPrototype(name, map);
|
||||
_maps.Add(name, prototype);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ namespace TanksServer.GameLogic;
|
|||
|
||||
internal sealed class SpawnPowerUp(
|
||||
IOptions<GameRules> options,
|
||||
MapEntityManager entityManager
|
||||
MapEntityManager entityManager,
|
||||
EmptyTileFinder emptyTileFinder
|
||||
) : ITickStep
|
||||
{
|
||||
private readonly double _spawnChance = options.Value.PowerUpSpawnChance;
|
||||
|
@ -34,7 +35,8 @@ internal sealed class SpawnPowerUp(
|
|||
_ => null
|
||||
};
|
||||
|
||||
entityManager.SpawnPowerUp(type, magazineType);
|
||||
var position = emptyTileFinder.ChooseEmptyTile().GetCenter().ToFloatPosition();
|
||||
entityManager.SpawnPowerUp(position, type, magazineType);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
21
tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs
Normal file
21
tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using System.IO;
|
||||
using TanksServer.Graphics;
|
||||
|
||||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class SpriteMapPrototype : MapPrototype
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly Sprite _sprite;
|
||||
|
||||
public SpriteMapPrototype(string name, Sprite sprite)
|
||||
{
|
||||
if (sprite.Width != MapService.PixelsPerRow || sprite.Height != MapService.PixelsPerColumn)
|
||||
throw new FileLoadException($"invalid image size in file {_name}");
|
||||
|
||||
_name = name;
|
||||
_sprite = sprite;
|
||||
}
|
||||
|
||||
public override Map CreateInstance() => new(_name, _sprite.ToBoolArray());
|
||||
}
|
|
@ -4,7 +4,8 @@ namespace TanksServer.GameLogic;
|
|||
|
||||
internal sealed class TankSpawnQueue(
|
||||
IOptions<GameRules> options,
|
||||
MapEntityManager entityManager
|
||||
MapEntityManager entityManager,
|
||||
EmptyTileFinder tileFinder
|
||||
) : ITickStep
|
||||
{
|
||||
private readonly ConcurrentQueue<Player> _queue = new();
|
||||
|
@ -25,7 +26,8 @@ internal sealed class TankSpawnQueue(
|
|||
if (!TryDequeueNext(out var player))
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
entityManager.SpawnTank(player);
|
||||
var position = tileFinder.ChooseEmptyTile().GetCenter().ToFloatPosition();
|
||||
entityManager.SpawnTank(player, position);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
|
|
37
tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs
Normal file
37
tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
namespace TanksServer.GameLogic;
|
||||
|
||||
internal sealed class TextMapPrototype : MapPrototype
|
||||
{
|
||||
private readonly string _name;
|
||||
private readonly string _text;
|
||||
|
||||
public TextMapPrototype(string name, string text)
|
||||
{
|
||||
if (text.Length != MapService.TilesPerColumn * MapService.TilesPerRow)
|
||||
throw new ArgumentException($"cannot load map {name}: invalid length");
|
||||
_name = name;
|
||||
_text = text;
|
||||
}
|
||||
|
||||
public override Map CreateInstance()
|
||||
{
|
||||
var walls = new bool[MapService.PixelsPerRow, MapService.PixelsPerColumn];
|
||||
|
||||
for (ushort tileX = 0; tileX < MapService.TilesPerRow; tileX++)
|
||||
for (ushort tileY = 0; tileY < MapService.TilesPerColumn; tileY++)
|
||||
{
|
||||
var tile = new TilePosition(tileX, tileY);
|
||||
if (_text[tileX + tileY * MapService.TilesPerRow] != '#')
|
||||
continue;
|
||||
|
||||
for (byte pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++)
|
||||
for (byte pixelInTileY = 0; pixelInTileY < MapService.TileSize; pixelInTileY++)
|
||||
{
|
||||
var (x, y) = tile.ToPixelPosition().GetPixelRelative(pixelInTileX, pixelInTileY);
|
||||
walls[x, y] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new Map(_name, walls);
|
||||
}
|
||||
}
|
|
@ -26,5 +26,17 @@ internal sealed class Sprite(bool?[,] data)
|
|||
public bool? this[int x, int y] => data[x, y];
|
||||
|
||||
public int Width => data.GetLength(0);
|
||||
|
||||
public int Height => data.GetLength(1);
|
||||
|
||||
public bool[,] ToBoolArray()
|
||||
{
|
||||
var result = new bool[Width, Height];
|
||||
|
||||
for (var y = 0; y < Height; y++)
|
||||
for (var x = 0; x < Width; x++)
|
||||
result[x, y] = this[x, y] ?? false;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,4 +38,7 @@ internal static class PositionHelpers
|
|||
pixelPosition.GetPixelRelative(add, add)
|
||||
);
|
||||
}
|
||||
|
||||
public static PixelPosition GetCenter(this TilePosition tile)
|
||||
=> tile.ToPixelPosition().GetPixelRelative(4, 4);
|
||||
}
|
||||
|
|
|
@ -64,11 +64,14 @@ public static class Program
|
|||
builder.Services.AddSingleton<TankSpawnQueue>();
|
||||
builder.Services.AddSingleton<Endpoints>();
|
||||
builder.Services.AddSingleton<BufferPool>();
|
||||
builder.Services.AddSingleton<EmptyTileFinder>();
|
||||
builder.Services.AddSingleton<ChangeToRequestedMap>();
|
||||
|
||||
builder.Services.AddHostedService<GameTickWorker>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ControlsServer>());
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ClientScreenServer>());
|
||||
|
||||
builder.Services.AddSingleton<ITickStep, ChangeToRequestedMap>(sp => sp.GetRequiredService<ChangeToRequestedMap>());
|
||||
builder.Services.AddSingleton<ITickStep, MoveBullets>();
|
||||
builder.Services.AddSingleton<ITickStep, CollideBullets>();
|
||||
builder.Services.AddSingleton<ITickStep, RotateTanks>();
|
||||
|
|
Loading…
Reference in a new issue