diff --git a/tank-frontend/src/PlayerInfo.tsx b/tank-frontend/src/PlayerInfo.tsx
index 4ab04bc..7f54c06 100644
--- a/tank-frontend/src/PlayerInfo.tsx
+++ b/tank-frontend/src/PlayerInfo.tsx
@@ -46,29 +46,35 @@ export default function PlayerInfo({player}: { player: string }) {
if (!lastJsonMessage || readyState !== ReadyState.OPEN)
return <>>;
+ let position = '';
+ if (lastJsonMessage.tank)
+ position = `(${Math.round(lastJsonMessage.tank.position.x)}|${Math.round(lastJsonMessage.tank.position.y)})`;
+
return
- Playing as {lastJsonMessage.name}
+ Playing as {lastJsonMessage.player.name}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
;
diff --git a/tank-frontend/src/serverCalls.tsx b/tank-frontend/src/serverCalls.tsx
index 83e4f2d..57ae781 100644
--- a/tank-frontend/src/serverCalls.tsx
+++ b/tank-frontend/src/serverCalls.tsx
@@ -14,24 +14,32 @@ export type Scores = {
readonly pixelsMoved: number;
};
-export type Player = {
- readonly name: string;
- readonly scores: Scores;
-};
-
-type TankInfo = {
- readonly magazine: string;
+type Tank = {
readonly position: { x: number; y: number };
readonly orientation: number;
readonly moving: boolean;
+ readonly bulletStats: BulletStats;
+ readonly reloadingUntil: string;
+ readonly nextShotAfter: string;
+ readonly magazine: {
+ readonly empty: boolean;
+ readonly usedBullets: number;
+ readonly maxBullets: number;
+ readonly displayString: string;
+ };
+}
+
+export type Player = {
+ readonly name: string;
+ readonly scores: Scores;
+ readonly openConnections: number;
+ readonly lastInput: string;
}
export type PlayerInfoMessage = {
- readonly name: string;
- readonly scores: Scores;
+ readonly player: Player;
readonly controls: string;
- readonly tank?: TankInfo;
- readonly openConnections: number;
+ readonly tank?: Tank;
}
export type MapInfo = {
@@ -40,6 +48,13 @@ export type MapInfo = {
readonly preview: string;
}
+export type BulletStats = {
+ speed: number;
+ acceleration: number,
+ explosive: boolean,
+ smart: boolean
+};
+
export function useMyWebSocket(url: string, options: Options = {}) {
return useWebSocket(url, {
shouldReconnect: () => true,
diff --git a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs
index 3b3571f..840c043 100644
--- a/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs
+++ b/tanks-backend/TanksServer/GameLogic/CollectPowerUp.cs
@@ -2,19 +2,27 @@ using System.Diagnostics;
namespace TanksServer.GameLogic;
-internal sealed class CollectPowerUp(
- MapEntityManager entityManager
-) : ITickStep
+internal sealed class CollectPowerUp : ITickStep
{
- private readonly Predicate _collectPredicate = b => TryCollect(b, entityManager.Tanks);
+ private readonly Predicate _collectPredicate;
+ private readonly GameRules _rules;
+ private readonly MapEntityManager _entityManager;
+
+ public CollectPowerUp(MapEntityManager entityManager,
+ IOptions options)
+ {
+ _entityManager = entityManager;
+ _rules = options.Value;
+ _collectPredicate = b => TryCollect(b, entityManager.Tanks);
+ }
public ValueTask TickAsync(TimeSpan delta)
{
- entityManager.RemoveWhere(_collectPredicate);
+ _entityManager.RemoveWhere(_collectPredicate);
return ValueTask.CompletedTask;
}
- private static bool TryCollect(PowerUp powerUp, IEnumerable tanks)
+ private bool TryCollect(PowerUp powerUp, IEnumerable tanks)
{
var position = powerUp.Position;
foreach (var tank in tanks)
@@ -34,32 +42,38 @@ internal sealed class CollectPowerUp(
return false;
}
- private static void ApplyPowerUpEffect(PowerUp powerUp, Tank tank)
+ private void ApplyPowerUpEffect(PowerUp powerUp, Tank tank)
{
switch (powerUp.Type)
{
- case PowerUpType.MagazineType:
- if (powerUp.MagazineType == null)
- throw new UnreachableException();
-
- tank.Magazine = tank.Magazine with
- {
- Type = tank.Magazine.Type | powerUp.MagazineType.Value,
- UsedBullets = 0
- };
-
- if (tank.ReloadingUntil >= DateTime.Now)
- tank.ReloadingUntil = DateTime.Now;
-
- break;
case PowerUpType.MagazineSize:
- tank.Magazine = tank.Magazine with
+ tank.MaxBullets = (byte)int.Clamp(tank.MaxBullets + 1, 1, 32);
+ break;
+
+ case PowerUpType.BulletAcceleration:
+ tank.BulletStats = tank.BulletStats with
{
- MaxBullets = (byte)int.Clamp(tank.Magazine.MaxBullets + 1, 1, 32)
+ Acceleration = tank.BulletStats.Acceleration * _rules.BulletAccelerationUpgradeStrength
};
break;
+
+ case PowerUpType.ExplosiveBullets:
+ tank.BulletStats = tank.BulletStats with { Explosive = true };
+ break;
+
+ case PowerUpType.SmartBullets:
+ tank.BulletStats = tank.BulletStats with { Smart = true };
+ break;
+
+ case PowerUpType.BulletSpeed:
+ tank.BulletStats = tank.BulletStats with
+ {
+ Speed = tank.BulletStats.Speed * _rules.BulletSpeedUpgradeStrength
+ };
+ break;
+
default:
- throw new UnreachableException();
+ throw new NotImplementedException($"unknown type {powerUp.Type}");
}
}
}
diff --git a/tanks-backend/TanksServer/GameLogic/CollideBullets.cs b/tanks-backend/TanksServer/GameLogic/CollideBullets.cs
index 4be0dca..c75fe6a 100644
--- a/tanks-backend/TanksServer/GameLogic/CollideBullets.cs
+++ b/tanks-backend/TanksServer/GameLogic/CollideBullets.cs
@@ -35,7 +35,7 @@ internal sealed class CollideBullets : ITickStep
if (bullet.Timeout > DateTime.Now)
return false;
- ExplodeAt(bullet.Position.ToPixelPosition(), bullet.IsExplosive, bullet.Owner);
+ ExplodeAt(bullet.Position.ToPixelPosition(), bullet.Stats.Explosive, bullet.Owner);
return true;
}
@@ -45,7 +45,7 @@ internal sealed class CollideBullets : ITickStep
if (!_map.Current.IsWall(pixel))
return false;
- ExplodeAt(pixel, bullet.IsExplosive, bullet.Owner);
+ ExplodeAt(pixel, bullet.Stats.Explosive, bullet.Owner);
return true;
}
@@ -55,7 +55,7 @@ internal sealed class CollideBullets : ITickStep
if (hitTank == null)
return false;
- ExplodeAt(bullet.Position.ToPixelPosition(), bullet.IsExplosive, bullet.Owner);
+ ExplodeAt(bullet.Position.ToPixelPosition(), bullet.Stats.Explosive, bullet.Owner);
return true;
}
diff --git a/tanks-backend/TanksServer/GameLogic/GameRules.cs b/tanks-backend/TanksServer/GameLogic/GameRules.cs
index 3660671..2d338b3 100644
--- a/tanks-backend/TanksServer/GameLogic/GameRules.cs
+++ b/tanks-backend/TanksServer/GameLogic/GameRules.cs
@@ -28,5 +28,7 @@ internal sealed class GameRules
public double SmartBulletInertia { get; set; } = 1;
- public double FastBulletAcceleration { get; set; } = 0.25;
+ public double BulletAccelerationUpgradeStrength { get; set; } = 0.1;
+
+ public double BulletSpeedUpgradeStrength { get; set; } = 0.1;
}
diff --git a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs
index 3b1f4f6..31cb28c 100644
--- a/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs
+++ b/tanks-backend/TanksServer/GameLogic/MapEntityManager.cs
@@ -15,19 +15,17 @@ internal sealed class MapEntityManager(
public IEnumerable Tanks => _playerTanks.Values;
public IEnumerable PowerUps => _powerUps;
- public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, MagazineType type)
+ public void SpawnBullet(Player tankOwner, FloatPosition position, double rotation, BulletStats stats)
{
_bullets.Add(new Bullet
{
Owner = tankOwner,
Position = position,
Rotation = rotation,
- IsExplosive = type.HasFlag(MagazineType.Explosive),
Timeout = DateTime.Now + _bulletTimeout,
OwnerCollisionAfter = DateTime.Now + TimeSpan.FromSeconds(1),
Speed = _rules.BulletSpeed,
- IsSmart = type.HasFlag(MagazineType.Smart),
- Acceleration = type.HasFlag(MagazineType.Fast) ? _rules.FastBulletAcceleration : 0d
+ Stats = stats
});
}
@@ -35,24 +33,23 @@ internal sealed class MapEntityManager(
public void SpawnTank(Player player, FloatPosition position)
{
- var tank = new Tank
+ var tank = new Tank(player)
{
- Owner = player,
Position = position,
Rotation = Random.Shared.NextDouble(),
- Magazine = new Magazine(MagazineType.Basic, 0, _rules.MagazineSize)
+ MaxBullets = _rules.MagazineSize,
+ BulletStats =new BulletStats(_rules.BulletSpeed, 0, false, false)
};
_playerTanks[player] = tank;
logger.LogInformation("Tank added for player {}", player.Name);
}
- public void SpawnPowerUp(FloatPosition position, PowerUpType type, MagazineType? magazineType)
+ public void SpawnPowerUp(FloatPosition position, PowerUpType type)
{
var powerUp = new PowerUp
{
Position = position,
- Type = type,
- MagazineType = magazineType
+ Type = type
};
_powerUps.Add(powerUp);
}
diff --git a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs
index d303d84..71e776f 100644
--- a/tanks-backend/TanksServer/GameLogic/MoveBullets.cs
+++ b/tanks-backend/TanksServer/GameLogic/MoveBullets.cs
@@ -17,14 +17,15 @@ internal sealed class MoveBullets(
private void MoveBullet(Bullet bullet, TimeSpan delta)
{
- if (bullet.IsSmart && TryGetSmartRotation(bullet.Position, bullet.Owner, out var wantedRotation))
+ if (bullet.Stats.Smart && TryGetSmartRotation(bullet.Position, bullet.Owner, out var wantedRotation))
{
var inertiaFactor = _smartBulletInertia * delta.TotalSeconds;
var difference = wantedRotation - bullet.Rotation;
bullet.Rotation += difference * inertiaFactor;
}
- bullet.Speed *= 1 + (bullet.Acceleration * delta.TotalSeconds);
+ bullet.Speed = double.Clamp(bullet.Speed * (1 + (bullet.Stats.Acceleration * delta.TotalSeconds)), 0d,
+ MapService.TileSize * 10);
var speed = bullet.Speed * delta.TotalSeconds;
var angle = bullet.Rotation * 2 * Math.PI;
diff --git a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs
index 9df8a64..3014c45 100644
--- a/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs
+++ b/tanks-backend/TanksServer/GameLogic/ShootFromTanks.cs
@@ -26,24 +26,17 @@ internal sealed class ShootFromTanks(
if (tank.ReloadingUntil >= now)
return;
- if (tank.Magazine.Empty)
+ if (tank.UsedBullets >= tank.MaxBullets)
{
tank.ReloadingUntil = now.AddMilliseconds(_config.ReloadDelayMs);
- tank.Magazine = tank.Magazine with
- {
- UsedBullets = 0,
- Type = MagazineType.Basic
- };
+ tank.UsedBullets = 0;
return;
}
tank.NextShotAfter = now.AddMilliseconds(_config.ShootDelayMs);
- tank.Magazine = tank.Magazine with
- {
- UsedBullets = (byte)(tank.Magazine.UsedBullets + 1)
- };
+ tank.UsedBullets++;
tank.Owner.Scores.ShotsFired++;
- entityManager.SpawnBullet(tank.Owner, tank.Position, tank.Orientation / 16d, tank.Magazine.Type);
+ entityManager.SpawnBullet(tank.Owner, tank.Position, tank.Orientation / 16d, tank.BulletStats);
}
}
diff --git a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs
index 72b0592..3107b71 100644
--- a/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs
+++ b/tanks-backend/TanksServer/GameLogic/SpawnPowerUp.cs
@@ -18,25 +18,9 @@ internal sealed class SpawnPowerUp(
if (Random.Shared.NextDouble() > _spawnChance * delta.TotalSeconds)
return ValueTask.CompletedTask;
-
- var type = Random.Shared.Next(4) == 0
- ? PowerUpType.MagazineSize
- : PowerUpType.MagazineType;
-
- MagazineType? magazineType = type switch
- {
- PowerUpType.MagazineType => Random.Shared.Next(0, 3) switch
- {
- 0 => MagazineType.Fast,
- 1 => MagazineType.Explosive,
- 2 => MagazineType.Smart,
- _ => throw new UnreachableException()
- },
- _ => null
- };
-
+ var type = (PowerUpType)Random.Shared.Next((int)Enum.GetValues().Max());
var position = emptyTileFinder.ChooseEmptyTile().GetCenter().ToFloatPosition();
- entityManager.SpawnPowerUp(position, type, magazineType);
+ entityManager.SpawnPowerUp(position, type);
return ValueTask.CompletedTask;
}
}
diff --git a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs
index 6d47060..c6125ec 100644
--- a/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs
+++ b/tanks-backend/TanksServer/Graphics/DrawPowerUpsStep.cs
@@ -14,12 +14,12 @@ internal sealed class DrawPowerUpsStep(MapEntityManager entityManager) : IDrawSt
{
foreach (var powerUp in entityManager.PowerUps)
{
- var sprite = powerUp switch
+ var sprite = powerUp.Type switch
{
- { Type: PowerUpType.MagazineSize } => _magazineSprite,
- { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Smart } => _smartSprite,
- { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Explosive } => _explosiveSprite,
- { Type: PowerUpType.MagazineType, MagazineType: MagazineType.Fast } => _fastSprite,
+ PowerUpType.MagazineSize => _magazineSprite,
+ PowerUpType.BulletAcceleration or PowerUpType.BulletSpeed => _fastSprite,
+ PowerUpType.SmartBullets => _smartSprite,
+ PowerUpType.ExplosiveBullets => _explosiveSprite,
_ => _genericSprite
};
diff --git a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs
index 04661c3..a81d2c4 100644
--- a/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs
+++ b/tanks-backend/TanksServer/Interactivity/PlayerInfoConnection.cs
@@ -47,20 +47,7 @@ internal sealed class PlayerInfoConnection
private async ValueTask?> GenerateMessageAsync()
{
var tank = _entityManager.GetCurrentTankOfPlayer(_player);
-
- TankInfo? tankInfo = null;
- if (tank != null)
- {
- var magazine = tank.ReloadingUntil > DateTime.Now ? "[ RELOADING ]" : tank.Magazine.ToDisplayString();
- tankInfo = new TankInfo(tank.Orientation, magazine, tank.Position.ToPixelPosition(), tank.Moving);
- }
-
- var info = new PlayerInfo(
- _player.Name,
- _player.Scores,
- _player.Controls.ToDisplayString(),
- tankInfo,
- _player.OpenConnections);
+ var info = new PlayerInfo(_player, _player.Controls.ToDisplayString(), tank);
_tempStream.Position = 0;
await JsonSerializer.SerializeAsync(_tempStream, info, AppSerializerContext.Default.PlayerInfo);
@@ -85,3 +72,9 @@ internal sealed class PlayerInfoConnection
Interlocked.Exchange(ref _lastMessage, data)?.Dispose();
}
}
+
+internal record struct PlayerInfo(
+ Player Player,
+ string Controls,
+ Tank? Tank
+);
diff --git a/tanks-backend/TanksServer/Models/Bullet.cs b/tanks-backend/TanksServer/Models/Bullet.cs
index c5f8d1d..aed3ddf 100644
--- a/tanks-backend/TanksServer/Models/Bullet.cs
+++ b/tanks-backend/TanksServer/Models/Bullet.cs
@@ -8,8 +8,6 @@ internal sealed class Bullet : IMapEntity
public required FloatPosition Position { get; set; }
- public required bool IsExplosive { get; init; }
-
public required DateTime Timeout { get; init; }
public PixelBounds Bounds => new(Position.ToPixelPosition(), Position.ToPixelPosition());
@@ -18,7 +16,5 @@ internal sealed class Bullet : IMapEntity
public required double Speed { get; set; }
- public required double Acceleration { get; init; }
-
- public required bool IsSmart { get; init; }
+ public required BulletStats Stats { get; init; }
}
diff --git a/tanks-backend/TanksServer/Models/Magazine.cs b/tanks-backend/TanksServer/Models/Magazine.cs
deleted file mode 100644
index febd5b8..0000000
--- a/tanks-backend/TanksServer/Models/Magazine.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Text;
-
-namespace TanksServer.Models;
-
-[Flags]
-internal enum MagazineType
-{
- Basic = 0,
- Fast = 1 << 0,
- Explosive = 1 << 1,
- Smart = 1 << 2,
-}
-
-internal readonly record struct Magazine(MagazineType Type, byte UsedBullets, byte MaxBullets)
-{
- public bool Empty => UsedBullets >= MaxBullets;
-
- public string ToDisplayString()
- {
- var sb = new StringBuilder();
-
- if (Type.HasFlag(MagazineType.Fast))
- sb.Append("» ");
- if (Type.HasFlag(MagazineType.Explosive))
- sb.Append("* ");
- if (Type.HasFlag(MagazineType.Smart))
- sb.Append("@ ");
-
- sb.Append("[ ");
- for (var i = 0; i < UsedBullets; i++)
- sb.Append("\u25cb ");
- for (var i = UsedBullets; i < MaxBullets; i++)
- sb.Append("• ");
- sb.Append(']');
-
- return sb.ToString();
- }
-}
diff --git a/tanks-backend/TanksServer/Models/PlayerInfo.cs b/tanks-backend/TanksServer/Models/PlayerInfo.cs
deleted file mode 100644
index a8545f3..0000000
--- a/tanks-backend/TanksServer/Models/PlayerInfo.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace TanksServer.Models;
-
-internal record struct TankInfo(
- int Orientation,
- string Magazine,
- PixelPosition Position,
- bool Moving
-);
-
-internal record struct PlayerInfo(
- string Name,
- Scores Scores,
- string Controls,
- TankInfo? Tank,
- int OpenConnections
-);
diff --git a/tanks-backend/TanksServer/Models/PowerUp.cs b/tanks-backend/TanksServer/Models/PowerUp.cs
index a8adb25..318a3a4 100644
--- a/tanks-backend/TanksServer/Models/PowerUp.cs
+++ b/tanks-backend/TanksServer/Models/PowerUp.cs
@@ -4,8 +4,11 @@ namespace TanksServer.Models;
internal enum PowerUpType
{
- MagazineType,
- MagazineSize
+ MagazineSize,
+ BulletSpeed,
+ BulletAcceleration,
+ ExplosiveBullets,
+ SmartBullets,
}
internal sealed class PowerUp: IMapEntity
@@ -15,6 +18,4 @@ internal sealed class PowerUp: IMapEntity
public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize);
public required PowerUpType Type { get; init; }
-
- public MagazineType? MagazineType { get; init; }
}
diff --git a/tanks-backend/TanksServer/Models/Tank.cs b/tanks-backend/TanksServer/Models/Tank.cs
index e9de961..ee955ea 100644
--- a/tanks-backend/TanksServer/Models/Tank.cs
+++ b/tanks-backend/TanksServer/Models/Tank.cs
@@ -1,13 +1,14 @@
using System.Diagnostics;
+using System.Text.Json.Serialization;
using TanksServer.GameLogic;
namespace TanksServer.Models;
-internal sealed class Tank : IMapEntity
+internal sealed class Tank(Player owner) : IMapEntity
{
private double _rotation;
- public required Player Owner { get; init; }
+ [JsonIgnore] public Player Owner { get; } = owner;
public double Rotation
{
@@ -26,11 +27,17 @@ internal sealed class Tank : IMapEntity
public required FloatPosition Position { get; set; }
- public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize);
+ [JsonIgnore] public PixelBounds Bounds => Position.GetBoundsForCenter(MapService.TileSize);
public int Orientation => (int)Math.Round(Rotation * 16) % 16;
- public required Magazine Magazine { get; set; }
+ public int UsedBullets { get; set; }
+
+ public int MaxBullets { get; set; }
public DateTime ReloadingUntil { get; set; }
+
+ public required BulletStats BulletStats { get; set; }
}
+
+internal sealed record class BulletStats(double Speed, double Acceleration, bool Explosive, bool Smart);