show tank infos in client
This commit is contained in:
parent
a50a9770c9
commit
0f4eec6343
|
@ -6,11 +6,19 @@ import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
function ScoreRow({name, value}: {
|
function ScoreRow({name, value}: {
|
||||||
name: string;
|
name: string;
|
||||||
value?: any;
|
value?: string | any;
|
||||||
}) {
|
}) {
|
||||||
|
let valueStr;
|
||||||
|
if (value === undefined)
|
||||||
|
valueStr = '?';
|
||||||
|
else if (typeof value === 'string' || value instanceof String)
|
||||||
|
valueStr = value;
|
||||||
|
else
|
||||||
|
valueStr = JSON.stringify(value);
|
||||||
|
|
||||||
return <tr>
|
return <tr>
|
||||||
<td>{name}</td>
|
<td>{name}</td>
|
||||||
<td>{value ?? '?'}</td>
|
<td>{valueStr}</td>
|
||||||
</tr>;
|
</tr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,35 +30,45 @@ type Controls = {
|
||||||
readonly shoot: boolean;
|
readonly shoot: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TankInfo = {
|
||||||
|
readonly explosiveBullets: number;
|
||||||
|
readonly position: { x: number; y: number };
|
||||||
|
readonly orientation: number;
|
||||||
|
readonly moving: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type PlayerInfoMessage = {
|
type PlayerInfoMessage = {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly scores: Scores;
|
readonly scores: Scores;
|
||||||
readonly controls: Controls;
|
readonly controls: Controls;
|
||||||
|
readonly tank?: TankInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function controlsString(controls: Controls) {
|
function controlsString(controls: Controls) {
|
||||||
let str = "";
|
let str = '';
|
||||||
if (controls.forward)
|
if (controls.forward)
|
||||||
str += "▲";
|
str += '▲';
|
||||||
if (controls.backward)
|
if (controls.backward)
|
||||||
str += "▼";
|
str += '▼';
|
||||||
if (controls.turnLeft)
|
if (controls.turnLeft)
|
||||||
str += "◄";
|
str += '◄';
|
||||||
if (controls.turnRight)
|
if (controls.turnRight)
|
||||||
str += "►";
|
str += '►';
|
||||||
if (controls.shoot)
|
if (controls.shoot)
|
||||||
str += "•";
|
str += '•';
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlayerInfo({playerId}: { playerId: Guid }) {
|
export default function PlayerInfo({playerId}: { playerId: Guid }) {
|
||||||
const [shouldSendMessage, setShouldSendMessage] = useState(true);
|
const [shouldSendMessage, setShouldSendMessage] = useState(false);
|
||||||
|
|
||||||
const url = makeApiUrl('/player');
|
const url = makeApiUrl('/player');
|
||||||
url.searchParams.set('id', playerId);
|
url.searchParams.set('id', playerId);
|
||||||
|
|
||||||
const {lastJsonMessage, readyState, sendMessage} = useWebSocket<PlayerInfoMessage>(url.toString(), {
|
const {lastJsonMessage, readyState, sendMessage} = useWebSocket<PlayerInfoMessage>(url.toString(), {
|
||||||
onMessage: () => setShouldSendMessage(true)
|
onMessage: () => setShouldSendMessage(true),
|
||||||
|
shouldReconnect: () => true,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -73,6 +91,10 @@ export default function PlayerInfo({playerId}: { playerId: Guid }) {
|
||||||
<ScoreRow name="kills" value={lastJsonMessage.scores.kills}/>
|
<ScoreRow name="kills" value={lastJsonMessage.scores.kills}/>
|
||||||
<ScoreRow name="deaths" value={lastJsonMessage.scores.deaths}/>
|
<ScoreRow name="deaths" value={lastJsonMessage.scores.deaths}/>
|
||||||
<ScoreRow name="walls" value={lastJsonMessage.scores.wallsDestroyed}/>
|
<ScoreRow name="walls" value={lastJsonMessage.scores.wallsDestroyed}/>
|
||||||
|
<ScoreRow name="explosive bullets" value={lastJsonMessage.tank?.explosiveBullets}/>
|
||||||
|
<ScoreRow name="position" value={lastJsonMessage.tank?.position}/>
|
||||||
|
<ScoreRow name="orientation" value={lastJsonMessage.tank?.orientation}/>
|
||||||
|
<ScoreRow name="moving" value={lastJsonMessage.tank?.moving}/>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</Column>;
|
</Column>;
|
||||||
|
|
|
@ -11,13 +11,13 @@ internal sealed class CollideBullets(
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan _)
|
public Task TickAsync(TimeSpan _)
|
||||||
{
|
{
|
||||||
entityManager.RemoveBulletsWhere(BulletHitsTank);
|
entityManager.RemoveWhere(BulletHitsTank);
|
||||||
entityManager.RemoveBulletsWhere(TryHitAndDestroyWall);
|
entityManager.RemoveWhere(BulletHitsWall);
|
||||||
entityManager.RemoveBulletsWhere(TimeoutBullet);
|
entityManager.RemoveWhere(BulletTimesOut);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TimeoutBullet(Bullet bullet)
|
private bool BulletTimesOut(Bullet bullet)
|
||||||
{
|
{
|
||||||
if (bullet.Timeout > DateTime.Now)
|
if (bullet.Timeout > DateTime.Now)
|
||||||
return false;
|
return false;
|
||||||
|
@ -27,7 +27,7 @@ internal sealed class CollideBullets(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryHitAndDestroyWall(Bullet bullet)
|
private bool BulletHitsWall(Bullet bullet)
|
||||||
{
|
{
|
||||||
var pixel = bullet.Position.ToPixelPosition();
|
var pixel = bullet.Position.ToPixelPosition();
|
||||||
if (!map.Current.IsWall(pixel))
|
if (!map.Current.IsWall(pixel))
|
||||||
|
|
|
@ -9,6 +9,7 @@ internal sealed class MapEntityManager(
|
||||||
private readonly HashSet<Bullet> _bullets = [];
|
private readonly HashSet<Bullet> _bullets = [];
|
||||||
private readonly HashSet<Tank> _tanks = [];
|
private readonly HashSet<Tank> _tanks = [];
|
||||||
private readonly HashSet<PowerUp> _powerUps = [];
|
private readonly HashSet<PowerUp> _powerUps = [];
|
||||||
|
private readonly Dictionary<Player, Tank> _playerTanks = [];
|
||||||
private readonly TimeSpan _bulletTimeout = TimeSpan.FromMilliseconds(options.Value.BulletTimeoutMs);
|
private readonly TimeSpan _bulletTimeout = TimeSpan.FromMilliseconds(options.Value.BulletTimeoutMs);
|
||||||
|
|
||||||
public IEnumerable<Bullet> Bullets => _bullets;
|
public IEnumerable<Bullet> Bullets => _bullets;
|
||||||
|
@ -31,14 +32,16 @@ internal sealed class MapEntityManager(
|
||||||
OwnerCollisionAfter = DateTime.Now + TimeSpan.FromSeconds(1),
|
OwnerCollisionAfter = DateTime.Now + TimeSpan.FromSeconds(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
public void RemoveBulletsWhere(Predicate<Bullet> predicate) => _bullets.RemoveWhere(predicate);
|
public void RemoveWhere(Predicate<Bullet> predicate) => _bullets.RemoveWhere(predicate);
|
||||||
|
|
||||||
public void SpawnTank(Player player)
|
public void SpawnTank(Player player)
|
||||||
{
|
{
|
||||||
_tanks.Add(new Tank(player, ChooseSpawnPosition())
|
var tank = new Tank(player, ChooseSpawnPosition())
|
||||||
{
|
{
|
||||||
Rotation = Random.Shared.NextDouble()
|
Rotation = Random.Shared.NextDouble()
|
||||||
});
|
};
|
||||||
|
_tanks.Add(tank);
|
||||||
|
_playerTanks[player] = tank;
|
||||||
logger.LogInformation("Tank added for player {}", player.Id);
|
logger.LogInformation("Tank added for player {}", player.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +53,7 @@ internal sealed class MapEntityManager(
|
||||||
{
|
{
|
||||||
logger.LogInformation("Tank removed for player {}", tank.Owner.Id);
|
logger.LogInformation("Tank removed for player {}", tank.Owner.Id);
|
||||||
_tanks.Remove(tank);
|
_tanks.Remove(tank);
|
||||||
|
_playerTanks.Remove(tank.Owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FloatPosition ChooseSpawnPosition()
|
public FloatPosition ChooseSpawnPosition()
|
||||||
|
@ -75,4 +79,6 @@ internal sealed class MapEntityManager(
|
||||||
var min = candidates.MaxBy(pair => pair.Value).Key;
|
var min = candidates.MaxBy(pair => pair.Value).Key;
|
||||||
return min.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
|
return min.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Tank? GetCurrentTankOfPlayer(Player player) => _playerTanks.GetValueOrDefault(player);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ internal sealed class MoveTanks(
|
||||||
public Task TickAsync(TimeSpan delta)
|
public Task TickAsync(TimeSpan delta)
|
||||||
{
|
{
|
||||||
foreach (var tank in entityManager.Tanks)
|
foreach (var tank in entityManager.Tanks)
|
||||||
tank.Moved = TryMoveTank(tank, delta);
|
tank.Moving = TryMoveTank(tank, delta);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ internal sealed class ShootFromTanks(
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan _)
|
public Task TickAsync(TimeSpan _)
|
||||||
{
|
{
|
||||||
foreach (var tank in entityManager.Tanks.Where(t => !t.Moved))
|
foreach (var tank in entityManager.Tanks.Where(t => !t.Moving))
|
||||||
Shoot(tank);
|
Shoot(tank);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using TanksServer.GameLogic;
|
||||||
|
|
||||||
namespace TanksServer.Interactivity;
|
namespace TanksServer.Interactivity;
|
||||||
|
|
||||||
internal sealed class PlayerInfoConnection(
|
internal sealed class PlayerInfoConnection(
|
||||||
Player player,
|
Player player,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
WebSocket rawSocket
|
WebSocket rawSocket,
|
||||||
|
MapEntityManager entityManager
|
||||||
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0)), IDisposable
|
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0)), IDisposable
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim _wantedFrames = new(1);
|
private readonly SemaphoreSlim _wantedFrames = new(1);
|
||||||
|
@ -45,7 +47,11 @@ internal sealed class PlayerInfoConnection(
|
||||||
|
|
||||||
private byte[]? GetMessageToSend()
|
private byte[]? GetMessageToSend()
|
||||||
{
|
{
|
||||||
var info = new PlayerInfo(player.Name, player.Scores, player.Controls);
|
var tank = entityManager.GetCurrentTankOfPlayer(player);
|
||||||
|
var tankInfo = tank != null
|
||||||
|
? new TankInfo(tank.Orientation, tank.ExplosiveBullets, tank.Position.ToPixelPosition(), tank.Moving)
|
||||||
|
: null;
|
||||||
|
var info = new PlayerInfo(player.Name, player.Scores, player.Controls, tankInfo);
|
||||||
var response = JsonSerializer.SerializeToUtf8Bytes(info, _context.PlayerInfo);
|
var response = JsonSerializer.SerializeToUtf8Bytes(info, _context.PlayerInfo);
|
||||||
|
|
||||||
if (response.SequenceEqual(_lastMessage))
|
if (response.SequenceEqual(_lastMessage))
|
||||||
|
|
|
@ -7,7 +7,8 @@ namespace TanksServer.Interactivity;
|
||||||
internal sealed class PlayerServer(
|
internal sealed class PlayerServer(
|
||||||
ILogger<PlayerServer> logger,
|
ILogger<PlayerServer> logger,
|
||||||
ILogger<PlayerInfoConnection> connectionLogger,
|
ILogger<PlayerInfoConnection> connectionLogger,
|
||||||
TankSpawnQueue tankSpawnQueue
|
TankSpawnQueue tankSpawnQueue,
|
||||||
|
MapEntityManager entityManager
|
||||||
) : WebsocketServer<PlayerInfoConnection>(logger), ITickStep
|
) : WebsocketServer<PlayerInfoConnection>(logger), ITickStep
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, Player> _players = new();
|
private readonly ConcurrentDictionary<string, Player> _players = new();
|
||||||
|
@ -46,7 +47,7 @@ internal sealed class PlayerServer(
|
||||||
public IEnumerable<Player> GetAll() => _players.Values;
|
public IEnumerable<Player> GetAll() => _players.Values;
|
||||||
|
|
||||||
public Task HandleClientAsync(WebSocket webSocket, Player player)
|
public Task HandleClientAsync(WebSocket webSocket, Player player)
|
||||||
=> HandleClientAsync(new PlayerInfoConnection(player, connectionLogger, webSocket));
|
=> HandleClientAsync(new PlayerInfoConnection(player, connectionLogger, webSocket, entityManager));
|
||||||
|
|
||||||
public Task TickAsync(TimeSpan delta)
|
public Task TickAsync(TimeSpan delta)
|
||||||
=> ParallelForEachConnectionAsync(connection => connection.OnGameTickAsync());
|
=> ParallelForEachConnectionAsync(connection => connection.OnGameTickAsync());
|
||||||
|
|
|
@ -1,3 +1,15 @@
|
||||||
namespace TanksServer.Models;
|
namespace TanksServer.Models;
|
||||||
|
|
||||||
internal sealed record class PlayerInfo(string Name, Scores Scores, PlayerControls Controls);
|
internal sealed record class TankInfo(
|
||||||
|
int Orientation,
|
||||||
|
byte ExplosiveBullets,
|
||||||
|
PixelPosition Position,
|
||||||
|
bool Moving
|
||||||
|
);
|
||||||
|
|
||||||
|
internal sealed record class PlayerInfo(
|
||||||
|
string Name,
|
||||||
|
Scores Scores,
|
||||||
|
PlayerControls Controls,
|
||||||
|
TankInfo? Tank
|
||||||
|
);
|
||||||
|
|
|
@ -22,7 +22,7 @@ internal sealed class Tank(Player player, FloatPosition spawnPosition) : IMapEnt
|
||||||
|
|
||||||
public DateTime NextShotAfter { get; set; }
|
public DateTime NextShotAfter { get; set; }
|
||||||
|
|
||||||
public bool Moved { get; set; }
|
public bool Moving { get; set; }
|
||||||
|
|
||||||
public FloatPosition Position { get; set; } = spawnPosition;
|
public FloatPosition Position { get; set; } = spawnPosition;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue