wip scores, components

This commit is contained in:
Vinzenz Schroeter 2024-04-13 23:07:08 +02:00
parent 64a61ef2b3
commit b604c01e22
20 changed files with 250 additions and 115 deletions

View file

@ -3,5 +3,7 @@
aspect-ratio: calc(352 / 160);
flex-grow: 1;
object-fit: contain;
max-width: 100%;
max-width: 100vw;
max-height: 100vh;
flex-shrink: 0;
}

View file

@ -1,7 +1,6 @@
import useWebSocket from 'react-use-websocket';
import {useEffect, useRef} from 'react';
import './ClientScreen.css';
import {statusTextForReadyState} from './statusTextForReadyState.tsx';
const pixelsPerRow = 352;
const pixelsPerCol = 160;
@ -41,11 +40,10 @@ function drawPixelsToCanvas(pixels: Uint8Array, canvas: HTMLCanvasElement) {
drawContext.putImageData(imageData, 0, 0);
}
export default function ClientScreen({logout}: {logout: () => void}) {
export default function ClientScreen({logout}: { logout: () => void }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const {
readyState,
lastMessage,
sendMessage,
getWebSocket
@ -67,8 +65,5 @@ export default function ClientScreen({logout}: {logout: () => void}) {
sendMessage('');
}, [lastMessage, canvasRef.current]);
return <>
<span>The screen is currently {statusTextForReadyState[readyState]}</span>
<canvas ref={canvasRef} id="screen" width={pixelsPerRow} height={pixelsPerCol}/>
</>;
return <canvas ref={canvasRef} id="screen" width={pixelsPerRow} height={pixelsPerCol}/>;
}

View file

@ -1,3 +1,7 @@
.Controls * {
box-sizing: content-box;
}
kbd {
background: hsl(0, 0%, 96%);
padding: 10px;

View file

@ -1,7 +1,7 @@
import './Controls.css';
import useWebSocket from 'react-use-websocket';
import {useEffect} from 'react';
import {statusTextForReadyState} from './statusTextForReadyState.tsx';
import Column from "./components/Column.tsx";
export default function Controls({playerId, logout}: {
playerId: string,
@ -12,7 +12,6 @@ export default function Controls({playerId, logout}: {
const {
sendMessage,
getWebSocket,
readyState
} = useWebSocket(url.toString(), {
onError: logout
});
@ -44,6 +43,7 @@ export default function Controls({playerId, logout}: {
if (!value)
return;
event.preventDefault();
const message = new Uint8Array([typeCode, value]);
sendMessage(message);
};
@ -60,8 +60,7 @@ export default function Controls({playerId, logout}: {
}, [sendMessage]);
return <>
<span>The controller is currently {statusTextForReadyState[readyState]}</span>
<div className="controls">
<Column className="Controls">
<div className="control">
<div className="row">
<kbd className="up"></kbd>
@ -71,12 +70,12 @@ export default function Controls({playerId, logout}: {
<kbd></kbd>
<kbd></kbd>
</div>
<h3>Move</h3>
</div>
<h3>Move</h3>
<div className="control">
<kbd className="space">Space</kbd>
<h3>Fire</h3>
</div>
</div>
<h3>Fire</h3>
</Column>
</>;
}

View file

@ -1,11 +1,4 @@
.TankWelcome {
display: flex;
flex-direction: column;
}
.JoinForm {
display: flex;
flex-direction: column;
border: 2px solid rgb(76, 76, 76);
border-radius: 4px;
}
@ -13,4 +6,4 @@
.JoinElems {
padding: 8px 8px;
margin: 8px 8px;
}
}

View file

@ -2,6 +2,7 @@ import {useEffect, useState} from 'react';
import './JoinForm.css';
import {NameId, PlayerResponse, postPlayer} from './serverCalls';
import {Guid} from './Guid.ts';
import Column from "./components/Column.tsx";
export default function JoinForm({setNameId, clientId}: {
setNameId: (mutator: (oldState: NameId) => NameId) => void,
@ -30,27 +31,25 @@ export default function JoinForm({setNameId, clientId}: {
}, [clicked, setData, data, clientId, setClicked, setNameId]);
const disableButtons = clicked || name.trim() === '';
return <div className="TankWelcome">
<h1 className="JoinElems" style={{'color': 'white'}}>
Tanks
</h1>
<p className="JoinElems" style={{'color': 'white'}}> Welcome and have fun!</p>
<div className="JoinForm">
<p className="JoinElems" style={{'color': 'white'}}>
Enter your name to join the game!
</p>
<input className="JoinElems"
type="text"
value={name}
placeholder="player name"
onChange={e => setName(e.target.value)}
/>
<button className="JoinElems"
onClick={() => setClicked(true)}
disabled={disableButtons}
>
join
</button>
</div>
</div>;
return <Column className='JoinForm'>
<p className="JoinElems"> Enter your name to join the game! </p>
<input
className="JoinElems"
type="text"
value={name}
placeholder="player name"
onChange={e => setName(e.target.value)}
onKeyUp={event => {
if (event.key === 'Enter')
setClicked(true);
}}
/>
<button
className="JoinElems"
onClick={() => setClicked(true)}
disabled={disableButtons}
>
join
</button>
</Column>;
}

View file

@ -1,21 +1,14 @@
.Player {
display: flex;
flex-direction: column;
.PlayerInfo {
border: 2px solid rgb(76, 76, 76);
border-radius: 4px;
}
.ScoreForm {
display: flex;
flex-direction: column;
}
.ElemGroup {
display: flex;
flex-direction: row;
}
.Elems {
padding: 8px 8px;
margin: 8px 8px;
}
}
.PlayerInfo-Reset {
height: 4em;
width: 4em;
}

View file

@ -1,53 +1,46 @@
import {useEffect, useState} from 'react';
import './PlayerInfo.css'
import {PlayerResponse, getPlayer} from './serverCalls';
import {Guid} from "./Guid.ts";
import Column from "./components/Column.tsx";
import Row from "./components/Row.tsx";
import Button from "./components/Button.tsx";
export default function PlayerInfo({playerId, logout}: { playerId: string, logout: () => void }) {
export default function PlayerInfo({playerId, logout, reset}: {
playerId: Guid,
logout: () => void,
reset: () => void
}) {
const [player, setPlayer] = useState<PlayerResponse | null>();
const refresh = () => {
getPlayer(playerId).then(value => {
if (value)
setPlayer(value);
else
logout();
});
};
useEffect(() => {
const refresh = () => {
getPlayer(playerId).then(value => {
if (value)
setPlayer(value);
else
logout();
});
};
const timer = setInterval(refresh, 5000);
return () => clearInterval(timer);
});
}, [playerId]);
return <div className='TankWelcome'>
<h1 className='Elems' style={{"color": "white"}}>
Tanks
</h1>
<div className="ScoreForm">
<div className='ElemGroup'>
<p className='Elems' style={{"color": "white"}}>
name:
</p>
<p className='Elems' style={{"color": "white"}}>
{player?.name}
</p>
</div>
<div className='ElemGroup'>
<p className='Elems' style={{"color": "white"}}>
kills:
</p>
<p className='Elems' style={{"color": "white"}}>
{player?.scores.kills}
</p>
</div>
<div className='ElemGroup'>
<p className='Elems' style={{"color": "white"}}>
deaths:
</p>
<p className='Elems' style={{"color": "white"}}>
{player?.scores.deaths}
</p>
</div>
</div>
</div>;
return <Column className='PlayerInfo'>
<Row>
<h3 className='grow'>
{player ? `Playing as "${player?.name}"` : 'loading...'}
</h3>
<Button className='PlayerInfo-Reset' onClick={() => reset()} text='x'/>
</Row>
<Row>
<p className='Elems'> kills: </p>
<p className='Elems'>{player?.scores.kills}</p>
</Row>
<Row>
<p className='Elems'>deaths: </p>
<p className='Elems'>{player?.scores.deaths}</p>
</Row>
</Column>;
}

View file

@ -0,0 +1,26 @@
import {getScores, PlayerResponse} from "./serverCalls.tsx";
import {useEffect, useState} from "react";
import DataTable from "./components/DataTable.tsx";
export default function Scoreboard({}: {}) {
const [players, setPlayers] = useState<PlayerResponse[]>([]);
useEffect(() => {
const refresh = () => {
getScores().then(value => {
if (value)
setPlayers(value);
});
};
const timer = setInterval(refresh, 5000);
return () => clearInterval(timer);
}, [players]);
return <DataTable<PlayerResponse> data={players} columns={[
{field: 'name'},
{field: 'kills', visualize: p => p.scores.kills},
{field: 'deaths', visualize: p => p.scores.deaths},
{field: 'k/d', visualize: p => p.scores.kills / p.scores.deaths}
]}/>
}

View file

@ -0,0 +1,7 @@
.Button {
border: solid 1px green;
border-radius: 12px;
color: green;
background: transparent;
}

View file

@ -0,0 +1,15 @@
import './Button.css';
import {MouseEventHandler} from "react";
export default function Button({text, onClick, className}: {
text: string,
onClick?: MouseEventHandler<HTMLButtonElement>,
className?: string
}) {
return <button
className={'Button ' + (className ?? '')}
onClick={onClick}
>
{text}
</button>
}

View file

@ -0,0 +1,7 @@
.Column {
display: flex;
flex-direction: column;
flex: auto;
max-height: 100%;
align-items: stretch;
}

View file

@ -0,0 +1,13 @@
import {ReactNode} from "react";
import './Column.css'
export default function Column({children, className}: {
children: ReactNode,
className?: string
}) {
return <div className={'Column ' + (className ?? '')}>
{children}
</div>
}

View file

@ -0,0 +1,3 @@
.DataTable {
flex-grow: 1;
}

View file

@ -0,0 +1,52 @@
import {ReactNode} from "react";
import './DataTable.css';
export type DataTableColumnDefinition<T> = {
field: string,
label?: string,
visualize?: (value: T) => ReactNode
};
function TableHead({columns}: { columns: DataTableColumnDefinition<any>[] }) {
return <thead className='DataTableHead'>
<tr>
{
columns.map(column =>
<th key={column.field}>
{column.label ?? column.field}
</th>)
}
</tr>
</thead>;
}
function DataTableRow({rowData, columns}: {
rowData: any,
columns: DataTableColumnDefinition<any>[]
}) {
return <tr>
{
columns.map(column => {
return <td key={column.field}>
{
column.visualize
? column.visualize(rowData)
: rowData[column.field]
}
</td>;
})
}
</tr>;
}
export default function DataTable<T>({data, columns}: {
data: T[],
columns: DataTableColumnDefinition<any>[]
}) {
return <table className='DataTable'>
<TableHead columns={columns}/>
<tbody>
{data.map((element, index) => <DataTableRow key={index} rowData={element} columns={columns}/>)}
</tbody>
</table>
}

View file

@ -0,0 +1,7 @@
.Row {
display: flex;
flex-direction: row;
flex: auto;
max-width: 100%;
align-items: stretch;
}

View file

@ -0,0 +1,9 @@
import {ReactNode} from "react";
import './Row.css';
export default function Row({children, className}: { children: ReactNode, className?: string }) {
return <div className={'Row ' + (className ?? '')}>
{children}
</div>
}

View file

@ -1,3 +1,6 @@
* {
box-sizing: border-box;
}
html, body {
height: 100%;
@ -11,12 +14,17 @@ body {
justify-content: center;
background-color: black;
color: white;
}
#root {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
width: 100%;
height: 100%;
width: 100vw;
height: 100vh;
}
.grow {
flex-grow: 1;
}

View file

@ -7,12 +7,17 @@ import {createRoot} from 'react-dom/client';
import PlayerInfo from './PlayerInfo.tsx';
import {useStoredObjectState} from './useStoredState.ts';
import {NameId, postPlayer} from './serverCalls.tsx';
import Column from "./components/Column.tsx";
import Row from "./components/Row.tsx";
import Scoreboard from "./Scoreboard.tsx";
const getNewNameId = () => ({
id: crypto.randomUUID(),
name: ''
});
function App() {
const [nameId, setNameId] = useStoredObjectState<NameId>('access', () => ({
id: crypto.randomUUID(),
name: ''
}));
const [nameId, setNameId] = useStoredObjectState<NameId>('access', getNewNameId);
const [isLoggedIn, setLoggedIn] = useState<boolean>(false);
const logout = () => setLoggedIn(false);
@ -24,12 +29,17 @@ function App() {
setLoggedIn(result !== null);
}, [nameId, isLoggedIn])();
return <>
return <Column className='grow'>
<h1>Tanks!</h1>
{nameId.name === '' && <JoinForm setNameId={setNameId} clientId={nameId.id}/>}
{isLoggedIn && <PlayerInfo playerId={nameId.id} logout={logout}/>}
<ClientScreen logout={logout}/>
{isLoggedIn && <Controls playerId={nameId.id} logout={logout}/>}
</>;
{isLoggedIn && <Row>
<Controls playerId={nameId.id} logout={logout}/>
<PlayerInfo playerId={nameId.id} logout={logout} reset={() => setNameId(getNewNameId)}/>
<Scoreboard/>
</Row>
}
</Column>;
}
createRoot(document.getElementById('root')!).render(

View file

@ -2,7 +2,7 @@ import {Guid} from './Guid.ts';
export type PlayerResponse = {
readonly name: string;
readonly id: string;
readonly id: Guid;
readonly scores: {
readonly kills: number;
readonly deaths: number;
@ -29,7 +29,7 @@ export function postPlayer({name, id}: NameId) {
return fetchTyped<NameId>({url, method: 'POST'});
}
export function getPlayer(id: string) {
export function getPlayer(id: Guid) {
const url = new URL(import.meta.env.VITE_TANK_PLAYER_URL);
url.searchParams.set('id', id);