working room creation and list

This commit is contained in:
Daniel Bulant 2022-07-27 16:40:26 +02:00
parent 7246ddea7b
commit 2206e2774f
17 changed files with 1787 additions and 26 deletions

View file

@ -24,5 +24,9 @@
"typescript": "^4.7.4",
"vite": "^3.0.0"
},
"type": "module"
"type": "module",
"dependencies": {
"howler": "^2.2.3",
"phaser": "^3.55.2"
}
}

View file

@ -4,6 +4,24 @@
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
<style>
body, html {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
overflow: hidden;
font-family: "Square", "Squareo", monospace;
}
@font-face {
font-family: 'Square';
src: url("/assets/Square.ttf") format("truetype");
}
@font-face {
font-family: 'Squareo';
src: url("/assets/Squareo.ttf") format("truetype");
}
</style>
%sveltekit.head%
</head>
<body>

View file

@ -126,7 +126,7 @@ class ConnectedClient extends EventTarget {
}
}
class WebsocketConnection extends EventTarget {
export class WebsocketConnection extends EventTarget {
ws: WebSocket;
fast: Map<string, ConnectedClient> = new Map();
roomName: string | null = null;
@ -140,9 +140,20 @@ class WebsocketConnection extends EventTarget {
}
connect() {
this.ws = new WebSocket("ws://" + location.hostname + ":8080");
this.ws.addEventListener("open", () => {
this.ws = new WebSocket("ws://" + location.hostname + ":8080/?name=" + encodeURIComponent(this.name));
this.ws.addEventListener("open", (e) => {
console.log("WS ready");
this.refreshList();
});
this.ws.addEventListener("close", (e) => {
console.log("WS closed");
lastError.set(e.reason || "Connection closed");
connection.set(null);
});
this.ws.addEventListener("error", (e) => {
console.error("WS error");
lastError.set("Connection error");
connection.set(null);
});
this.ws.addEventListener("message", (e) => {
const msg = JSON.parse(e.data);
@ -193,32 +204,54 @@ class WebsocketConnection extends EventTarget {
}
this.fast.set(client, fast);
}
// missing break on purpose
}
case "create": {
this.roomName = msg.name;
this.roomId = msg.id;
room.set({
name: msg.name,
id: msg.id
});
break;
}
case "leave": {
const fast = this.fast.get(msg.name);
if (!fast) return;
fast.conn.close();
this.fast.delete(msg.name);
break;
}
case "left": {
console.log("Left room successfully");
this.roomName = null;
this.roomId = null;
room.set(null);
this.fast.forEach(connection => connection.conn.close());
this.fast = new Map();
break;
}
case "list": {
list.set(msg.rooms);
listLoading.set(false);
break;
}
case "error": {
console.error(msg.e);
lastError.set(msg.e);
break;
}
}
});
}
createGame(name: string) {
this.ws.send(JSON.stringify({ t: "create", name: name }));
}
refreshList() {
this.ws.send(JSON.stringify({ t: "list" }));
listLoading.set(true);
}
send(data: any) {
@ -227,4 +260,7 @@ class WebsocketConnection extends EventTarget {
}
export const connection: Writable<WebsocketConnection|null> = writable(null);
export const list: Writable<{ id: string, name: string, count: number }[]|null> = writable(null);
export const list: Writable<{ id: string, name: string, count: number }[]|null> = writable(null);
export const listLoading = writable(true);
export const lastError: Writable<string> = writable("");
export const room: Writable<{ name: string, id: string }|null> = writable(null);

View file

View file

@ -0,0 +1,49 @@
import { CANVAS, Game, Scale, WEBGL } from "phaser";
import { GameScene } from "./scene";
var ratio = window.devicePixelRatio || 1;
export function resize() {
if(!game || !htmlcanvas) return;
try {
game.scale.resize(htmlcanvas.parentElement!.clientWidth * ratio, htmlcanvas.parentElement!.clientHeight * ratio);
} catch(e) {
// @ts-ignore
console.error(e, new ErrorEvent(e.type, { colno: e.colno, error: e, lineno: e.lineno, message: e.message, filename: e.filename }));
window.dispatchEvent(new ErrorEvent("error", e as any));
}
// console.log("size", htmlcanvas.parentElement!.clientWidth * ratio, htmlcanvas.parentElement!.clientHeight * ratio);
}
/** @type {HTMLCanvasElement} */
var htmlcanvas: HTMLCanvasElement;
/** @type {Game} */
var game: Game;
/** @type {GameScene} */
var gs: GameScene;
export function setCanvas(canvas: HTMLCanvasElement) {
htmlcanvas = canvas;
var ctx = canvas.getContext("webgl2") || canvas.getContext("webgl");
gs = new GameScene();
game = new Game({
canvas: canvas,
url: window.location.host,
hideBanner: true,
type: ctx ? WEBGL : CANVAS,
// @ts-ignore
context: ctx || canvas.getContext("2d"),
customEnvironment: false,
width: window.innerWidth * ratio,
height: window.innerHeight * ratio,
scale: {
mode: Scale.RESIZE
},
physics: {
default: "arcade",
},
title: "Multidie",
version: "0",
scene: [gs],
backgroundColor: "#01021B",
banner: false
});
}

View file

@ -0,0 +1,28 @@
import Phaser, { Animations } from "phaser";
var fpsBuffer: number[] = [];
export class GameScene extends Phaser.Scene {
constructor() {
super({
key: "GameScene",
active: true,
physics: {
default: "arcade"
}
});
}
unload() {}
preload() {}
create() {}
update(time: number, delta: number) {
fpsBuffer.push(delta);
if (fpsBuffer.length > 10) {
fpsBuffer.shift();
}
const fps = fpsBuffer.reduce((a, b) => a + b, 0) / fpsBuffer.length;
}
}

View file

@ -0,0 +1,151 @@
<script lang="ts">
import { connection, lastError, list, listLoading } from "$lib/Websocket";
import { onMount } from "svelte";
onMount(() => {
let i = setInterval(() => {
$connection?.refreshList();
}, 2000);
return () => clearInterval(i);
});
let creatingGame = false;
let newGameName = "";
let error = "";
$: {
if (newGameName.length > 64) {
error = "Name is too long";
} else {
error = "";
}
}
$: {
if($lastError === "room_name_used") {
error = $lastError;
$lastError = "";
}
}
function submit() {
if (newGameName.length < 2) return error = "Name is too short";
if (newGameName.length > 64) return error = "Name is too long";
error = "Creating game...";
$connection!.createGame(newGameName);
}
</script>
{#if creatingGame}
<dialog open>
<input on:submit={submit} type="text" name="name" placeholder="GAME NAME" bind:value={newGameName}>
<div class="error">
{error}
</div>
<div class="controls">
<button on:click={() => creatingGame = false}>CANCEL</button>
<button on:click={submit}>SUBMIT</button>
</div>
</dialog>
{/if}
<div class="container">
<main>
<div class="flex">
<h1>Games - {$connection?.name}</h1>
<button on:click={() => creatingGame = true}>CREATE</button>
</div>
<div class="status">
{#if $listLoading} Loading... {/if}
</div>
<ul>
{#if $list}
{#each $list as $game}
<li class="flex">
<span>
{$game.name}
</span>
<span>
{$game.count}
</span>
</li>
{/each}
{/if}
</ul>
</main>
</div>
<style>
dialog {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(20px) saturate(150%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 1.5rem;
gap: 0.25em;
}
dialog input {
font-size: 1.5rem;
background-color: #85E65C;
color: #bd5ce6;
border: 10px solid #bd5ce6;
border-radius: 0;
}
.container {
width: 100vw;
height: 100vh;
background-color: #85E65C;
color: #bd5ce6;
}
main {
max-width: 700px;
margin: auto;
}
.flex {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.status {
height: 1.5em;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
height: 2em;
line-height: 2em;
padding: 0.5em;
border: 1px solid #bd5ce6;
border-radius: 0;
margin: 0;
font-size: 1.5rem;
cursor: pointer;
}
button {
width: 10em;
height: 3em;
border: 1px solid #bd5ce6;
border-radius: 0;
padding: 0.5em;
font-size: 1.5rem;
border: 10px solid #bd5ce6;
background-color: #85E65C;
cursor: pointer;
}
button:active {
background-color: #bd5ce6;
color: #85E65C;
}
</style>

View file

@ -0,0 +1,76 @@
<script>
import { connection, lastError, WebsocketConnection } from "$lib/Websocket";
var name = "";
var error = "";
$: {
if(name.length > 64) {
error = "Name is too long";
} else {
error = "";
}
}
function submit() {
if (name.length < 2) return error = "Name is too short";
if (name.length > 64) return error = "Name is too long";
$connection = new WebsocketConnection(name);
}
</script>
<div class="container">
<main>
<input on:submit={submit} type="text" name="name" placeholder="PLAYER NAME" bind:value={name}>
<div class="error">
{error || $lastError}
</div>
<button on:click={submit}>SUBMIT</button>
</main>
</div>
<style>
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
.container {
width: 100vw;
height: 100vh;
background-color: #85E65C;
color: #bd5ce6;
}
.error {
height: 1.5em;
}
input {
width: 20em;
height: 2em;
border: 10px solid #bd5ce6;
border-radius: 0;
padding: 0.5em;
font-size: 1.5em;
background-color: #85E65C;
color: #bd5ce6;
font-family: inherit;
}
button {
width: 10em;
height: 3em;
border: 10px solid #bd5ce6;
background-color: #85E65C;
color: #bd5ce6;
border-radius: 0;
padding: 0.5em;
font-size: 1.5em;
font-family: inherit;
cursor: pointer;
}
button:active {
background-color: #bd5ce6;
color: #85E65C;
}
</style>

View file

@ -1,2 +1,14 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
<script>
import Game from "$lib/view/game/game.svelte";
import List from "$lib/view/menu/list.svelte";
import NameChoose from "$lib/view/menu/nameChoose.svelte";
import { connection, room } from "$lib/Websocket";
</script>
{#if !$connection}
<NameChoose />
{:else if !$room}
<List />
{:else}
<Game />
{/if}

View file

@ -8,6 +8,11 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
"strict": true,
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -33,23 +33,19 @@ const clients = new Map();
require("uWebSockets.js")
.App({})
.ws("/", {
idleTimeout: 30,
idleTimeout: 32,
maxPayloadLength: 16 * 1024 * 1024,
upgrade: (res, req, context) => {
console.log(
`An Http connection wants to become WebSocket, URL: ${req.getUrl()}!`
);
const url = req.getUrl();
const parsed = new URL(url);
let name = parsed.searchParams.get("name");
let name = req.getQuery("name").trim().toLocaleLowerCase();
if (!name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim()) return res.end("invalid_name");
name = name.trim();
if (clients.forEach(client => client.name === name)) return res.end("name_used");
if ([...clients.values()].find(client => client.name === name)) return res.end("name_used");
/* This immediately calls open handler, you must not use res after this call */
res.upgrade(
{
url,
name
},
/* Spell these correctly */
@ -62,13 +58,13 @@ require("uWebSockets.js")
open: (ws) => {
console.log(
"CON",
ws.url,
ws.name,
decoder.decode(ws.getRemoteAddressAsText())
);
clients.set(ws, {
connection: ws,
name: ws.name,
ipa: decoder.decode(ws.getRemoteAddressAsText()),
room: null
});
},
@ -83,9 +79,10 @@ require("uWebSockets.js")
}
case "create": {
const client = clients.get(ws);
if(client.room) return ws.send(JSON.stringify({t: "error", m: "already_in_room"}));
const name = data.name.trim();
if (!name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim()) return res.send(JSON.stringify({t: "error", m: "invalid_room_name"}));
if(client.room) return ws.send(JSON.stringify({t: "error", e: "already_in_room"}));
const name = data.name.trim().toLocaleLowerCase();
if (!name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim()) return res.send(JSON.stringify({t: "error", e: "invalid_room_name"}));
if ([...rooms.values()].find(room => room.name === name)) return ws.send(JSON.stringify({t: "error", e: "room_name_used"}));
const room = {
name: name,
host: ws,
@ -99,8 +96,8 @@ require("uWebSockets.js")
case "leave": {
const client = clients.get(ws);
const room = client.room;
if (!room) return ws.send(JSON.stringify({ t: "error", m: "room_not_found" }));
if (!room.clients.includes(client)) return ws.send(JSON.stringify({ t: "error", m: "not_in_room" }));
if (!room) return ws.send(JSON.stringify({ t: "error", e: "room_not_found" }));
if (!room.clients.includes(client)) return ws.send(JSON.stringify({ t: "error", e: "not_in_room" }));
room.clients.splice(room.clients.indexOf(client), 1);
if (room.clients.length === 0) {
rooms.delete(room.id);
@ -116,13 +113,15 @@ require("uWebSockets.js")
case "join": {
const client = clients.get(ws);
if (!client) return ws.end();
const room = client.room;
const room = rooms.get(msg.room);
if (!room) return ws.send(JSON.stringify({ t: "error", e: "room_not_found" }));
if(client.room) return ws.send(JSON.stringify({ t: "error", e: "already_in_other_room" }));
if (room.clients.includes(client)) return ws.send(JSON.stringify({ t: "error", e: "already_in_room" }));
if(room.clients.length > 5) return ws.send(JSON.stringify({ t: "error", e: "room_full" }));
room.clients.push(ws);
ws.room = room;
room.clients.slice(0, -2).forEach(client => client.connection.send(JSON.stringify({ t: "join", id: room.id, client: client.name })));
ws.send(JSON.stringify({ t: "joined", id: room.id, client: client.name, clients: room.clients.map(t => t.name) }));
ws.send(JSON.stringify({ t: "joined", id: room.id, name: room.name, client: client.name, clients: room.clients.map(t => t.name) }));
break;
}
case "cand":
@ -138,7 +137,7 @@ require("uWebSockets.js")
targetClient.connection.send(JSON.stringify({ t: msg.t, id: room.id, source: client.name, d: msg.d }));
}
case "list": {
ws.send(JSON.stringify({ t: "list", rooms: [...rooms.values()].map(t => ({ id: t.id, name: t.name, count: t.clients.length }))}));
ws.send(JSON.stringify({ t: "list", rooms: [...rooms.values()].filter(t => t.clients.length < 5).map(t => ({ id: t.id, name: t.name, count: t.clients.length }))}));
break;
}
}
@ -147,9 +146,11 @@ require("uWebSockets.js")
}
},
close: (ws, code, message) => {
console.log("DIS", decoder.decode(ws.getRemoteAddressAsText()));
console.log("DIS1", ws);
if (clients.get(ws)) {
let room = rooms.get(clients.get(ws));
const client = clients.get(ws);
console.log("DIS", client.name, client.ipa);
let room = client.room;
if (room) {
room.clients.splice(room.clients.indexOf(ws), 1);
if (room.clients.length === 0) {