From 5f5e9fb716a3eaf0c4017330b8333fe0b1210ff0 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 5 May 2024 13:51:28 +0200 Subject: [PATCH 1/9] add endpoint for requesting map data --- tanks-backend/TanksServer/Endpoints.cs | 21 +++++++- .../TanksServer/GameLogic/MapPrototype.cs | 8 +++ .../TanksServer/GameLogic/MapService.cs | 51 ++++++++++++------- .../GameLogic/SpriteMapPrototype.cs | 13 ++--- .../TanksServer/GameLogic/TextMapPrototype.cs | 14 ++--- .../TanksServer/Graphics/DrawMapStep.cs | 18 ++++++- .../Interactivity/AppSerializerContext.cs | 1 + tanks-backend/TanksServer/Models/MapInfo.cs | 3 ++ 8 files changed, 97 insertions(+), 32 deletions(-) create mode 100644 tanks-backend/TanksServer/GameLogic/MapPrototype.cs create mode 100644 tanks-backend/TanksServer/Models/MapInfo.cs diff --git a/tanks-backend/TanksServer/Endpoints.cs b/tanks-backend/TanksServer/Endpoints.cs index a65b582..0eb0100 100644 --- a/tanks-backend/TanksServer/Endpoints.cs +++ b/tanks-backend/TanksServer/Endpoints.cs @@ -24,14 +24,18 @@ internal sealed class Endpoints( app.Map("/controls", ConnectControlsAsync); app.MapGet("/map", () => mapService.MapNames); app.MapPost("/map", PostMap); + app.MapGet("/map/{name}", GetMapByName); } private Results, NotFound, Ok> PostMap([FromQuery] string name) { if (string.IsNullOrWhiteSpace(name)) return TypedResults.BadRequest("invalid map name"); - if (!mapService.TryGetMapByName(name, out var map)) + + name = name.Trim().ToUpperInvariant(); + if (!mapService.TryGetPrototype(name, out var map)) return TypedResults.NotFound("map with name not found"); + changeToRequestedMap.Request(map); return TypedResults.Ok(); } @@ -88,4 +92,19 @@ internal sealed class Endpoints( var player = playerService.GetOrAdd(name); return TypedResults.Ok(player.Name); } + + private Results, NotFound, BadRequest> GetMapByName(string name) + { + name = name.Trim().ToUpperInvariant(); + if (string.IsNullOrEmpty(name)) + return TypedResults.BadRequest("map name cannot be empty"); + + if (!mapService.TryGetPrototype(name, out var prototype)) + return TypedResults.NotFound(); + if (!mapService.TryGetPreview(name, out var preview)) + return TypedResults.NotFound(); + + var mapInfo = new MapInfo(prototype.Name, prototype.GetType().Name, preview.Data); + return TypedResults.Ok(mapInfo); + } } diff --git a/tanks-backend/TanksServer/GameLogic/MapPrototype.cs b/tanks-backend/TanksServer/GameLogic/MapPrototype.cs new file mode 100644 index 0000000..cff453d --- /dev/null +++ b/tanks-backend/TanksServer/GameLogic/MapPrototype.cs @@ -0,0 +1,8 @@ +namespace TanksServer.GameLogic; + +internal abstract class MapPrototype +{ + public abstract string Name { get; } + + public abstract Map CreateInstance(); +} diff --git a/tanks-backend/TanksServer/GameLogic/MapService.cs b/tanks-backend/TanksServer/GameLogic/MapService.cs index dccf166..b9263f4 100644 --- a/tanks-backend/TanksServer/GameLogic/MapService.cs +++ b/tanks-backend/TanksServer/GameLogic/MapService.cs @@ -1,14 +1,10 @@ using System.Diagnostics.CodeAnalysis; using System.IO; +using DisplayCommands; using TanksServer.Graphics; namespace TanksServer.GameLogic; -internal abstract class MapPrototype -{ - public abstract Map CreateInstance(); -} - internal sealed class MapService { public const ushort TilesPerRow = 44; @@ -17,9 +13,10 @@ internal sealed class MapService public const ushort PixelsPerRow = TilesPerRow * TileSize; public const ushort PixelsPerColumn = TilesPerColumn * TileSize; - private readonly Dictionary _maps = new(); + private readonly Dictionary _mapPrototypes = new(); + private readonly Dictionary _mapPreviews = new(); - public IEnumerable MapNames => _maps.Keys; + public IEnumerable MapNames => _mapPrototypes.Keys; public Map Current { get; private set; } @@ -29,29 +26,49 @@ internal sealed class MapService LoadMapString(file); foreach (var file in Directory.EnumerateFiles("./assets/maps/", "*.png")) LoadMapPng(file); - - var chosenMapIndex = Random.Shared.Next(_maps.Count); - var chosenMapName = _maps.Keys.Skip(chosenMapIndex).First(); - Current = _maps[chosenMapName].CreateInstance(); + Current = GetRandomMap(); } - public bool TryGetMapByName(string name, [MaybeNullWhen(false)] out MapPrototype map) - => _maps.TryGetValue(name, out map); + public bool TryGetPrototype(string name, [MaybeNullWhen(false)] out MapPrototype map) + => _mapPrototypes.TryGetValue(name, out map); public void SwitchTo(MapPrototype prototype) => Current = prototype.CreateInstance(); + public bool TryGetPreview(string name, [MaybeNullWhen(false)] out PixelGrid pixelGrid) + { + if (_mapPreviews.TryGetValue(name, out pixelGrid)) + return true; // already generated + if (!_mapPrototypes.TryGetValue(name, out var prototype)) + return false; // name not found + + pixelGrid = new PixelGrid(PixelsPerRow, PixelsPerColumn); + DrawMapStep.Draw(pixelGrid, prototype.CreateInstance()); + + _mapPreviews.TryAdd(name, pixelGrid); // another thread may have added the map already + return true; // new preview + } + private void LoadMapPng(string file) { - var name = Path.GetFileName(file); + var name = MapNameFromFilePath(file); var prototype = new SpriteMapPrototype(name, Sprite.FromImageFile(file)); - _maps.Add(Path.GetFileName(file), prototype); + _mapPrototypes.Add(name, prototype); } private void LoadMapString(string file) { + var name = MapNameFromFilePath(file); var map = File.ReadAllText(file).ReplaceLineEndings(string.Empty).Trim(); - var name = Path.GetFileName(file); var prototype = new TextMapPrototype(name, map); - _maps.Add(name, prototype); + _mapPrototypes.Add(name, prototype); + } + + private static string MapNameFromFilePath(string filePath) => Path.GetFileName(filePath).ToUpperInvariant(); + + private Map GetRandomMap() + { + var chosenMapIndex = Random.Shared.Next(_mapPrototypes.Count); + var chosenMapName = _mapPrototypes.Keys.Skip(chosenMapIndex).First(); + return _mapPrototypes[chosenMapName].CreateInstance(); } } diff --git a/tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs b/tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs index 8d383eb..202bf98 100644 --- a/tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs +++ b/tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs @@ -5,17 +5,18 @@ namespace TanksServer.GameLogic; internal sealed class SpriteMapPrototype : MapPrototype { - private readonly string _name; - private readonly Sprite _sprite; + public override string Name { get; } + + public Sprite Sprite { get; } public SpriteMapPrototype(string name, Sprite sprite) { if (sprite.Width != MapService.PixelsPerRow || sprite.Height != MapService.PixelsPerColumn) - throw new FileLoadException($"invalid image size in file {_name}"); + throw new FileLoadException($"invalid image size in file {Name}"); - _name = name; - _sprite = sprite; + Name = name; + Sprite = sprite; } - public override Map CreateInstance() => new(_name, _sprite.ToBoolArray()); + public override Map CreateInstance() => new(Name, Sprite.ToBoolArray()); } diff --git a/tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs b/tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs index 3195453..21f95d8 100644 --- a/tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs +++ b/tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs @@ -2,17 +2,19 @@ namespace TanksServer.GameLogic; internal sealed class TextMapPrototype : MapPrototype { - private readonly string _name; - private readonly string _text; + public override string Name { get; } + + public string Text { get; } 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; + Name = name; + Text = text; } + public override Map CreateInstance() { var walls = new bool[MapService.PixelsPerRow, MapService.PixelsPerColumn]; @@ -21,7 +23,7 @@ internal sealed class TextMapPrototype : MapPrototype for (ushort tileY = 0; tileY < MapService.TilesPerColumn; tileY++) { var tile = new TilePosition(tileX, tileY); - if (_text[tileX + tileY * MapService.TilesPerRow] != '#') + if (Text[tileX + tileY * MapService.TilesPerRow] != '#') continue; for (byte pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++) @@ -32,6 +34,6 @@ internal sealed class TextMapPrototype : MapPrototype } } - return new Map(_name, walls); + return new Map(Name, walls); } } diff --git a/tanks-backend/TanksServer/Graphics/DrawMapStep.cs b/tanks-backend/TanksServer/Graphics/DrawMapStep.cs index cf23bf0..3be6a37 100644 --- a/tanks-backend/TanksServer/Graphics/DrawMapStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawMapStep.cs @@ -1,18 +1,32 @@ +using DisplayCommands; using TanksServer.GameLogic; namespace TanksServer.Graphics; internal sealed class DrawMapStep(MapService map) : IDrawStep { - public void Draw(GamePixelGrid pixels) + public void Draw(GamePixelGrid pixels) => Draw(pixels, map.Current); + + private static void Draw(GamePixelGrid pixels, Map map) { for (ushort y = 0; y < MapService.PixelsPerColumn; y++) for (ushort x = 0; x < MapService.PixelsPerRow; x++) { - if (!map.Current.IsWall(x, y)) + if (!map.IsWall(x, y)) continue; pixels[x, y].EntityType = GamePixelEntityType.Wall; } } + + public static void Draw(PixelGrid pixels, Map map) + { + for (ushort y = 0; y < MapService.PixelsPerColumn; y++) + for (ushort x = 0; x < MapService.PixelsPerRow; x++) + { + if (!map.IsWall(x, y)) + continue; + pixels[x, y] = true; + } + } } diff --git a/tanks-backend/TanksServer/Interactivity/AppSerializerContext.cs b/tanks-backend/TanksServer/Interactivity/AppSerializerContext.cs index 004c313..e9560d5 100644 --- a/tanks-backend/TanksServer/Interactivity/AppSerializerContext.cs +++ b/tanks-backend/TanksServer/Interactivity/AppSerializerContext.cs @@ -7,5 +7,6 @@ namespace TanksServer.Interactivity; [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(PlayerInfo))] +[JsonSerializable(typeof(MapInfo))] [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)] internal sealed partial class AppSerializerContext : JsonSerializerContext; diff --git a/tanks-backend/TanksServer/Models/MapInfo.cs b/tanks-backend/TanksServer/Models/MapInfo.cs new file mode 100644 index 0000000..637f8bb --- /dev/null +++ b/tanks-backend/TanksServer/Models/MapInfo.cs @@ -0,0 +1,3 @@ +namespace TanksServer.Models; + +public record MapInfo(string Name, string TypeName, Memory Preview); From 5f27739e7ca0013cb8802d635a8a6f154ea9a7cb Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 5 May 2024 13:51:59 +0200 Subject: [PATCH 2/9] move types to serverCalls.tsx --- tank-frontend/src/PlayerInfo.tsx | 17 +---------------- tank-frontend/src/serverCalls.tsx | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tank-frontend/src/PlayerInfo.tsx b/tank-frontend/src/PlayerInfo.tsx index 2916d04..4ab04bc 100644 --- a/tank-frontend/src/PlayerInfo.tsx +++ b/tank-frontend/src/PlayerInfo.tsx @@ -1,4 +1,4 @@ -import {makeApiUrl, Scores, useMyWebSocket} from './serverCalls'; +import {makeApiUrl, PlayerInfoMessage, useMyWebSocket} from './serverCalls'; import Column from './components/Column.tsx'; import {ReadyState} from 'react-use-websocket'; import {useEffect, useState} from 'react'; @@ -21,21 +21,6 @@ function ScoreRow({name, value}: { ; } -type TankInfo = { - readonly magazine: string; - readonly position: { x: number; y: number }; - readonly orientation: number; - readonly moving: boolean; -} - -type PlayerInfoMessage = { - readonly name: string; - readonly scores: Scores; - readonly controls: string; - readonly tank?: TankInfo; - readonly openConnections: number; -} - export default function PlayerInfo({player}: { player: string }) { const [shouldSendMessage, setShouldSendMessage] = useState(false); diff --git a/tank-frontend/src/serverCalls.tsx b/tank-frontend/src/serverCalls.tsx index 6e003fb..66675fd 100644 --- a/tank-frontend/src/serverCalls.tsx +++ b/tank-frontend/src/serverCalls.tsx @@ -19,6 +19,21 @@ export type Player = { readonly scores: Scores; }; +type TankInfo = { + readonly magazine: string; + readonly position: { x: number; y: number }; + readonly orientation: number; + readonly moving: boolean; +} + +export type PlayerInfoMessage = { + readonly name: string; + readonly scores: Scores; + readonly controls: string; + readonly tank?: TankInfo; + readonly openConnections: number; +} + export function useMyWebSocket(url: string, options: Options = {}) { return useWebSocket(url, { shouldReconnect: () => true, From 5fc21494ef17d7bb1cf7f7144c06be5595ceb555 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 5 May 2024 14:08:44 +0200 Subject: [PATCH 3/9] extract PixelGridCanvas component from ClientScreen --- tank-frontend/src/ClientScreen.tsx | 155 +----------------- .../PixelGridCanvas.css} | 2 +- .../src/components/PixelGridCanvas.tsx | 149 +++++++++++++++++ 3 files changed, 159 insertions(+), 147 deletions(-) rename tank-frontend/src/{ClientScreen.css => components/PixelGridCanvas.css} (88%) create mode 100644 tank-frontend/src/components/PixelGridCanvas.tsx diff --git a/tank-frontend/src/ClientScreen.tsx b/tank-frontend/src/ClientScreen.tsx index ba78edd..ee6bab2 100644 --- a/tank-frontend/src/ClientScreen.tsx +++ b/tank-frontend/src/ClientScreen.tsx @@ -1,107 +1,14 @@ -import {useEffect, useRef, useState} from 'react'; -import './ClientScreen.css'; -import {hslToString, Theme} from './theme.ts'; +import {useEffect, useState} from 'react'; +import {Theme} from './theme.ts'; import {makeApiUrl, useMyWebSocket} from './serverCalls.tsx'; import {ReadyState} from 'react-use-websocket'; +import PixelGridCanvas from './components/PixelGridCanvas.tsx'; -const pixelsPerRow = 352; -const pixelsPerCol = 160; -const observerMessageSize = pixelsPerCol * pixelsPerRow / 8; - -enum GamePixelEntityType { - Wall = 0x0, - Tank = 0x1, - Bullet = 0x2 -} - -function getPixelDataIndexes(bitIndex: number) { - return { - byteIndex: Math.floor(bitIndex / 8), - bitInByteIndex: 7 - bitIndex % 8 - }; -} - -function normalizeColor(context: CanvasRenderingContext2D, color: string) { - context.fillStyle = color; - context.fillRect(0, 0, 1, 1); - return context.getImageData(0, 0, 1, 1).data; -} - -function parseAdditionalDataNibble(nibble: number) { - const isPlayerMask = 1; - const entityTypeMask = 12; - - return { - isCurrentPlayer: (nibble & isPlayerMask) != 0, - entityType: ((nibble & entityTypeMask) >> 2) as GamePixelEntityType, - }; -} - -function drawPixelsToCanvas( - { - context, width, height, pixels, additional, foreground, background, playerColor, otherTanksColor - }: { - context: CanvasRenderingContext2D, - width: number, - height: number, - pixels: Uint8ClampedArray, - additional: Uint8ClampedArray | null, - background: Uint8ClampedArray, - foreground: Uint8ClampedArray, - playerColor: Uint8ClampedArray, - otherTanksColor: Uint8ClampedArray - } -) { - let additionalDataIndex = 0; - let additionalDataByte: number | null = null; - const nextPixelColor = (isOn: boolean) => { - if (!isOn) - return background; - if (!additional) - return foreground; - - let info; - if (additionalDataByte === null) { - additionalDataByte = additional[additionalDataIndex]; - additionalDataIndex++; - info = parseAdditionalDataNibble(additionalDataByte); - } else { - info = parseAdditionalDataNibble(additionalDataByte >> 4); - additionalDataByte = null; - } - - if (info.isCurrentPlayer) - return playerColor; - - if (info.entityType == GamePixelEntityType.Tank) - return otherTanksColor; - - return foreground; - }; - - const imageData = context.getImageData(0, 0, width, height, {colorSpace: 'srgb'}); - const data = imageData.data; - - for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const pixelIndex = y * pixelsPerRow + x; - const {byteIndex, bitInByteIndex} = getPixelDataIndexes(pixelIndex); - const isOn = (pixels[byteIndex] & (1 << bitInByteIndex)) !== 0; - const color = nextPixelColor(isOn); - - for (let colorChannel of [0, 1, 2, 3]) - data[pixelIndex * 4 + colorChannel] = color[colorChannel]; - } - } - - context.putImageData(imageData, 0, 0); -} export default function ClientScreen({theme, player}: { theme: Theme, player: string | null }) { - const canvasRef = useRef(null); const [shouldSendMessage, setShouldSendMessage] = useState(false); const url = makeApiUrl('/screen', 'ws'); @@ -114,7 +21,8 @@ export default function ClientScreen({theme, player}: { getWebSocket, readyState } = useMyWebSocket(url.toString(), { - onOpen: _ => setShouldSendMessage(true) + onOpen: _ => setShouldSendMessage(true), + onMessage: _ => setShouldSendMessage(true) }); const socket = getWebSocket(); @@ -128,54 +36,9 @@ export default function ClientScreen({theme, player}: { sendMessage(''); }, [readyState, shouldSendMessage]); + if (!lastMessage) + return <>; - useEffect(() => { - if (lastMessage === null) - return; - - let ignore = false; - const start = async () => { - const canvas = canvasRef.current; - if (canvas === null) - throw new Error('canvas null'); - - const drawContext = canvas.getContext('2d'); - if (!drawContext) - throw new Error('could not get draw context'); - - let pixels = new Uint8ClampedArray(lastMessage.data); - let additionalData: Uint8ClampedArray | null = null; - if (pixels.length > observerMessageSize) { - additionalData = pixels.slice(observerMessageSize); - pixels = pixels.slice(0, observerMessageSize); - } - - if (ignore) - return; - - drawPixelsToCanvas({ - context: drawContext, - width: canvas.width, - height: canvas.height, - pixels, - additional: additionalData, - background: normalizeColor(drawContext, hslToString(theme.background)), - foreground: normalizeColor(drawContext, hslToString(theme.primary)), - playerColor: normalizeColor(drawContext, hslToString(theme.secondary)), - otherTanksColor: normalizeColor(drawContext, hslToString(theme.tertiary)) - }); - - if (ignore) - return; - - setShouldSendMessage(true); - }; - - start(); - return () => { - ignore = true; - }; - }, [lastMessage, canvasRef.current, theme]); - - return ; + const pixels = new Uint8ClampedArray(lastMessage.data); + return ; } diff --git a/tank-frontend/src/ClientScreen.css b/tank-frontend/src/components/PixelGridCanvas.css similarity index 88% rename from tank-frontend/src/ClientScreen.css rename to tank-frontend/src/components/PixelGridCanvas.css index 3304737..316596e 100644 --- a/tank-frontend/src/ClientScreen.css +++ b/tank-frontend/src/components/PixelGridCanvas.css @@ -1,5 +1,5 @@ -#screen { +.PixelGridCanvas { aspect-ratio: calc(352 / 160); flex-grow: 1; object-fit: contain; diff --git a/tank-frontend/src/components/PixelGridCanvas.tsx b/tank-frontend/src/components/PixelGridCanvas.tsx new file mode 100644 index 0000000..b1471e9 --- /dev/null +++ b/tank-frontend/src/components/PixelGridCanvas.tsx @@ -0,0 +1,149 @@ +import {hslToString, Theme} from '../theme.ts'; +import {useEffect, useRef} from 'react'; +import './PixelGridCanvas.css'; + +const pixelsPerRow = 352; +const pixelsPerCol = 160; +const observerMessageSize = pixelsPerCol * pixelsPerRow / 8; + +enum GamePixelEntityType { + Wall = 0x0, + Tank = 0x1, + Bullet = 0x2 +} + +function getPixelDataIndexes(bitIndex: number) { + return { + byteIndex: Math.floor(bitIndex / 8), + bitInByteIndex: 7 - bitIndex % 8 + }; +} + +function normalizeColor(context: CanvasRenderingContext2D, color: string) { + context.fillStyle = color; + context.fillRect(0, 0, 1, 1); + return context.getImageData(0, 0, 1, 1).data; +} + +function parseAdditionalDataNibble(nibble: number) { + const isPlayerMask = 1; + const entityTypeMask = 12; + + return { + isCurrentPlayer: (nibble & isPlayerMask) != 0, + entityType: ((nibble & entityTypeMask) >> 2) as GamePixelEntityType, + }; +} + +function drawPixelsToCanvas( + { + context, width, height, pixels, additional, foreground, background, playerColor, otherTanksColor + }: { + context: CanvasRenderingContext2D, + width: number, + height: number, + pixels: Uint8ClampedArray, + additional: Uint8ClampedArray | null, + background: Uint8ClampedArray, + foreground: Uint8ClampedArray, + playerColor: Uint8ClampedArray, + otherTanksColor: Uint8ClampedArray + } +) { + let additionalDataIndex = 0; + let additionalDataByte: number | null = null; + const nextPixelColor = (isOn: boolean) => { + if (!isOn) + return background; + if (!additional) + return foreground; + + let info; + if (additionalDataByte === null) { + additionalDataByte = additional[additionalDataIndex]; + additionalDataIndex++; + info = parseAdditionalDataNibble(additionalDataByte); + } else { + info = parseAdditionalDataNibble(additionalDataByte >> 4); + additionalDataByte = null; + } + + if (info.isCurrentPlayer) + return playerColor; + + if (info.entityType == GamePixelEntityType.Tank) + return otherTanksColor; + + return foreground; + }; + + const imageData = context.getImageData(0, 0, width, height, {colorSpace: 'srgb'}); + const data = imageData.data; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const pixelIndex = y * pixelsPerRow + x; + const {byteIndex, bitInByteIndex} = getPixelDataIndexes(pixelIndex); + const isOn = (pixels[byteIndex] & (1 << bitInByteIndex)) !== 0; + const color = nextPixelColor(isOn); + + for (let colorChannel of [0, 1, 2, 3]) + data[pixelIndex * 4 + colorChannel] = color[colorChannel]; + } + } + + context.putImageData(imageData, 0, 0); +} + +export default function PixelGridCanvas({pixels, theme}: { + readonly pixels: Uint8ClampedArray; + readonly theme: Theme; +}) { + const canvasRef = useRef(null); + + useEffect(() => { + let ignore = false; + const start = async () => { + const canvas = canvasRef.current; + if (canvas === null) + throw new Error('canvas null'); + + const drawContext = canvas.getContext('2d'); + if (!drawContext) + throw new Error('could not get draw context'); + + let additionalData: Uint8ClampedArray | null = null; + if (pixels.length > observerMessageSize) { + additionalData = pixels.slice(observerMessageSize); + pixels = pixels.slice(0, observerMessageSize); + } + + if (ignore) + return; + + drawPixelsToCanvas({ + context: drawContext, + width: canvas.width, + height: canvas.height, + pixels, + additional: additionalData, + background: normalizeColor(drawContext, hslToString(theme.background)), + foreground: normalizeColor(drawContext, hslToString(theme.primary)), + playerColor: normalizeColor(drawContext, hslToString(theme.secondary)), + otherTanksColor: normalizeColor(drawContext, hslToString(theme.tertiary)) + }); + }; + + start(); + return () => { + ignore = true; + }; + }, [pixels, canvasRef.current, theme]); + + return ; +} From a952d4227d966a21ed7f6ab109fbf554c8fce737 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 5 May 2024 22:33:55 +0200 Subject: [PATCH 4/9] extract dialog component --- tank-frontend/src/JoinForm.css | 12 ------------ tank-frontend/src/JoinForm.tsx | 11 ++++++----- tank-frontend/src/components/Dialog.css | 13 +++++++++++++ tank-frontend/src/components/Dialog.tsx | 12 ++++++++++++ 4 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 tank-frontend/src/components/Dialog.css create mode 100644 tank-frontend/src/components/Dialog.tsx diff --git a/tank-frontend/src/JoinForm.css b/tank-frontend/src/JoinForm.css index a1baee0..151848c 100644 --- a/tank-frontend/src/JoinForm.css +++ b/tank-frontend/src/JoinForm.css @@ -9,18 +9,6 @@ } .JoinForm { - border: solid var(--border-size-thick) var(--color-secondary); - - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - background: var(--color-background); - - gap: 16px; - padding: 16px; - animation-duration: 1s; animation-name: BlinkJoinFormBorder; animation-iteration-count: infinite; diff --git a/tank-frontend/src/JoinForm.tsx b/tank-frontend/src/JoinForm.tsx index dfd85b6..2235387 100644 --- a/tank-frontend/src/JoinForm.tsx +++ b/tank-frontend/src/JoinForm.tsx @@ -1,10 +1,11 @@ import {useState} from 'react'; -import './JoinForm.css'; +import {useMutation} from '@tanstack/react-query'; + import {makeApiUrl} from './serverCalls'; -import Column from './components/Column.tsx'; import Button from './components/Button.tsx'; import TextInput from './components/TextInput.tsx'; -import {useMutation} from '@tanstack/react-query'; +import Dialog from './components/Dialog.tsx'; +import './JoinForm.css'; export default function JoinForm({onDone}: { onDone: (name: string) => void; @@ -29,7 +30,7 @@ export default function JoinForm({onDone}: { const confirm = () => postPlayer.mutate({name}); - return + return

Enter your name to play

{postPlayer.isError &&

{postPlayer.error.message}

} -
; + ; } diff --git a/tank-frontend/src/components/Dialog.css b/tank-frontend/src/components/Dialog.css new file mode 100644 index 0000000..193905f --- /dev/null +++ b/tank-frontend/src/components/Dialog.css @@ -0,0 +1,13 @@ + +.Dialog { + border: solid var(--border-size-thick) var(--color-secondary); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + background: var(--color-background); + + gap: 16px; + padding: 16px; +} diff --git a/tank-frontend/src/components/Dialog.tsx b/tank-frontend/src/components/Dialog.tsx new file mode 100644 index 0000000..960efc3 --- /dev/null +++ b/tank-frontend/src/components/Dialog.tsx @@ -0,0 +1,12 @@ +import {ReactNode} from 'react'; +import Column from './Column.tsx'; +import './Dialog.css'; + +export default function Dialog({children, className}: { + children: ReactNode; + className?: string; +}) { + return + {children} + +} From 170a7faf0f6ac38030c91c1d3b4b0024bec35033 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 5 May 2024 23:33:08 +0200 Subject: [PATCH 5/9] map chooser dialog with previews --- tank-frontend/src/App.tsx | 6 +- tank-frontend/src/MapChooser.css | 14 +++ tank-frontend/src/MapChooser.tsx | 117 +++++++++++++++++++----- tank-frontend/src/components/Column.tsx | 11 ++- tank-frontend/src/components/Dialog.css | 5 + tank-frontend/src/components/Dialog.tsx | 2 +- tank-frontend/src/serverCalls.tsx | 6 ++ 7 files changed, 131 insertions(+), 30 deletions(-) diff --git a/tank-frontend/src/App.tsx b/tank-frontend/src/App.tsx index 1496522..f5e11e6 100644 --- a/tank-frontend/src/App.tsx +++ b/tank-frontend/src/App.tsx @@ -8,8 +8,8 @@ import Scoreboard from './Scoreboard.tsx'; import Button from './components/Button.tsx'; import MapChooser from './MapChooser.tsx'; import './App.css'; -import {getRandomTheme, useStoredTheme} from "./theme.ts"; -import { useState } from 'react'; +import {getRandomTheme, useStoredTheme} from './theme.ts'; +import {useState} from 'react'; export default function App() { const [theme, setTheme] = useStoredTheme(); @@ -21,7 +21,7 @@ export default function App() {

CCCB-Tanks!

- +