initial wip

This commit is contained in:
Daniel Bulant 2023-01-19 14:27:18 +01:00
parent 62fa17b953
commit 21a3ae9ea3
22 changed files with 1182 additions and 1974 deletions

View file

@ -13,20 +13,24 @@
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"prettier": "^2.6.2",
"prettier-plugin-svelte": "^2.7.0",
"svelte": "^3.44.0",
"svelte-check": "^2.7.1",
"svelte-preprocess": "^4.10.6",
"tslib": "^2.3.1",
"typescript": "^4.7.4",
"vite": "^3.0.0"
"@sveltejs/adapter-auto": "1.0.2",
"@sveltejs/kit": "1.1.4",
"@unocss/transformer-variant-group": "^0.48.4",
"prettier": "^2.8.3",
"prettier-plugin-svelte": "^2.9.0",
"svelte": "^3.55.1",
"svelte-check": "^2.10.3",
"svelte-preprocess": "^4.10.7",
"tslib": "^2.4.1",
"typescript": "^4.9.4",
"vite": "^4.0.4"
},
"type": "module",
"dependencies": {
"@unocss/core": "^0.48.4",
"@unocss/transformer-directives": "^0.48.4",
"howler": "^2.2.3",
"phaser": "^3.55.2"
"phaser": "^3.55.2",
"unocss": "^0.48.4"
}
}

View file

@ -1,358 +0,0 @@
import { writable, type Writable } from "svelte/store";
class FastEvent extends Event {
data: any;
constructor(name: string, data: any) {
super(name);
this.data = data;
}
}
const hosts: { urls: string, credential?: string, username?: string }[] = [
{
urls: "stun:openrelay.metered.ca:80",
},
{
urls: "turn:openrelay.metered.ca:80",
username: "openrelayproject",
credential: "openrelayproject",
},
{
urls: "turn:openrelay.metered.ca:443",
username: "openrelayproject",
credential: "openrelayproject",
},
{
urls: "turn:openrelay.metered.ca:443?transport=tcp",
username: "openrelayproject",
credential: "openrelayproject",
}
]
class ConnectedClient extends EventTarget {
conn: RTCPeerConnection;
sendChannel: RTCDataChannel;
candidates: any[] = [];
state: RTCDataChannelState | null = null;
readyState: number = 0;
pings: number[] = [];
score: number = 0;
lives: number = 3;
lastScoreChange: number = 0;
constructor(public ws: WebsocketConnection, public name: string) {
super();
// @ts-ignore Initialized in the next function call
this.conn = null;
// @ts-ignore
this.sendChannel = null;
this.initializeConnection();
}
initializeConnection() {
this.pings = [];
console.log("Initializing connection");
this.conn = new RTCPeerConnection({
iceServers: hosts
});
this.conn.onicecandidate = e => {
console.log("candidate", e, e.candidate);
if (!e.candidate) return;
this.candidates.push(e.candidate);
this.ws.send(JSON.stringify({ t: "cand", target: this.name, d: e.candidate }));
};
this.conn.onicecandidateerror = (e) => console.error(e);
this.conn.ondatachannel = e => this.onDataChannel(e.channel);
}
onDataChannel(channel: RTCDataChannel) {
console.log("on data channel");
this.sendChannel = channel;
let timer: any;
this.sendChannel.onclose = (e) => {
clearInterval(timer);
this.statusChanged();
}
this.sendChannel.onopen = (e) => {
timer = setInterval(() => {
this.send({ t: "p", d: Date.now() });
}, 100);
this.statusChanged();
}
this.statusChanged();
this.sendChannel.onmessage = (e) => {
const msg = JSON.parse(e.data);
switch (msg.t) {
case "p":
this.send({
t: "pr",
d: msg.d,
y: Date.now()
})
break;
case "pr":
this.pings.push(Date.now() - msg.d);
if(this.pings.length > 15) this.pings = this.pings.slice(-15);
players.update(t => t);
break;
case "msg":
console.log("message", msg.d);
this.dispatchEvent(new FastEvent("message", msg.d));
messages.update(t => { t.push({ author: this.name, content: msg.d });return t})
break;
case "lives":
this.lives = msg.d;
players.update(t => t);
break;
case "score":
this.score = msg.d;
this.lastScoreChange = Date.now();
players.update(t => t);
break;
case "start":
gameData.set({ score: 0, lives: 3, lastScoreChange: 0 });
// break not on purpose
default:
console.log("MSG", msg);
this.dispatchEvent(new FastEvent(msg.t, msg.d));
}
};
}
send(data: any) {
this.sendChannel.send(JSON.stringify(data));
}
statusChanged() {
if (this.sendChannel) {
if (this.state !== this.sendChannel.readyState) {
if (this.state === "open" && ["closing", "closed"].includes(this.sendChannel.readyState)) {
this.initializeConnection();
}
this.state = this.sendChannel.readyState;
if (this.state === "open") this.readyState = 3;
if (["closing", "closed"].includes(this.state)) this.readyState = 4;
console.log("state", this.state);
players.update(t => t);
}
}
}
}
export class WebsocketConnection extends EventTarget {
ws: WebSocket;
fast: Map<string, ConnectedClient> = new Map();
roomName: string | null = null;
roomHost: string | null = null;
constructor(public name: string) {
super();
// @ts-ignore Initialized in the next function call
this.ws = null;
this.connect();
players.set(this.fast);
}
connect() {
const host = location.hostname.includes("danbulant.eu") ? "wss://multidie.danbulant.cloud" : "ws://" + location.hostname + ":8080";
this.ws = new WebSocket(host + "/?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);
room.set(null);
list.set(null);
});
this.ws.addEventListener("error", (e) => {
console.error("WS error");
lastError.set("Connection error");
connection.set(null);
room.set(null);
list.set(null);
});
this.ws.addEventListener("message", (e) => {
const msg = JSON.parse(e.data);
console.log(msg);
switch (msg.t) {
case "cand": {
const fast = this.fast.get(msg.source);
if (!fast) return console.log("No fast connection");
if (fast.readyState < 1) fast.readyState == 1;
players.set(this.fast);
console.log("Received candidates");
if (fast.state === "open") return console.log("Already open");
fast.conn.addIceCandidate(msg.d).then();
break;
}
case "desc": {
const fast = this.fast.get(msg.source);
if (!fast) return console.log("No fast connection");
if (fast.readyState < 2) fast.readyState == 2;
players.set(this.fast);
if (fast.state === "open") return console.log("Already open");
if (msg.d.type === "answer") {
fast.conn.setRemoteDescription(msg.d);
} else if (msg.d.type === "offer") {
fast.conn.setRemoteDescription(msg.d)
.then(() => fast.conn.createAnswer())
.then(answer => fast.conn.setLocalDescription(answer))
.then(() =>
this.ws.send(JSON.stringify({ t: "desc", target: fast.name, d: fast.conn.localDescription }))
)
}
break;
}
case "join": {
const fast = new ConnectedClient(this, msg.client);
players.set(this.fast);
this.fast.set(msg.client, fast);
if (fast.candidates && fast.candidates.length) {
for (const candidate of fast.candidates) {
this.ws.send(JSON.stringify({ t: "cand", target: msg.client, d: candidate }));
}
}
messages.update(t => { t.push({ author: " SYS ", content: `${msg.client} joined`});return t})
break;
}
case "joined": {
const clients = msg.clients;
this.fast = new Map();
for (const client of clients) {
if (client === this.name) continue;
const fast = new ConnectedClient(this, client);
fast.conn.createOffer()
.then(offer => fast.conn.setLocalDescription(offer))
.then(() =>
this.ws.send(JSON.stringify({ t: "desc", target: client, d: fast.conn.localDescription }))
);
fast.sendChannel = fast.conn.createDataChannel("sendChannel");
fast.onDataChannel(fast.sendChannel);
this.fast.set(client, fast);
}
players.set(this.fast);
messages.set([{
author: " SYS ", content: `${msg.client} joined`
}]);
this.roomName = msg.name;
this.roomHost = msg.host;
room.set({
name: msg.name,
host: msg.host
});
break;
}
case "create": {
this.roomName = msg.name;
this.roomHost = this.name;
room.set({
name: msg.name,
host: this.name
});
messages.update(t => { t.push({ author: " SYS ", content: `${msg.name} created the room`});return t})
break;
}
case "leave": {
const fast = this.fast.get(msg.client);
if (!fast) return;
fast.conn.close();
this.fast.delete(msg.client);
players.set(this.fast);
messages.update(t => { t.push({ author: " SYS ", content: `${msg.client} left`});return t})
if(this.fast.size == 0) {
gameData.set(null);
}
break;
}
case "host": {
this.roomHost = msg.host;
room.update(t => { t!.host = this.roomHost!; return t });
messages.update(t => { t.push({ author: " SYS ", content: `${msg.host} is now host`});return t})
break;
}
case "left": {
console.log("Left room successfully");
this.roomName = null;
room.set(null);
this.fast.forEach(connection => connection.conn.close());
this.fast = new Map();
players.set(this.fast);
messages.set([]);
break;
}
case "list": {
list.set(msg.rooms);
listLoading.set(false);
break;
}
case "error": {
console.error(msg.e);
lastError.set(msg.e);
break;
}
}
});
}
sendMessage(msg: string) {
if (!this.roomName) return console.log("Not in a room");
this.broadcast({ t: "msg", d: msg });
messages.update(t => { t.push({ author: this.name, content: msg }); return t });
}
broadcast(data: any) {
if (!this.roomName) return console.log("Not in a room");
for(const [, client] of this.fast) {
client.send(data);
}
}
setScore(score: number) {
if (!this.roomName) return console.log("Not in a room");
this.broadcast({ t: "score", d: score });
gameData.update(t => { t!.score = score; t!.lastScoreChange = Date.now(); return t});
}
setLives(lives: number) {
if (!this.roomName) return console.log("Not in a room");
this.broadcast({ t: "lives", d: lives });
gameData.update(t => { t!.lives = lives; return t});
}
createGame(name: string) {
this.ws.send(JSON.stringify({ t: "create", name: name }));
}
startGame() {
if (!this.roomName) return console.log("Not in a room");
this.broadcast({ t: "start" });
for(const [, client] of this.fast) {
client.score = 0;
}
gameData.set({ score: 0, lives: 3, lastScoreChange: 0 });
}
join(name: string) {
this.ws.send(JSON.stringify({ t: "join", name: name }));
}
refreshList() {
this.ws.send(JSON.stringify({ t: "list" }));
listLoading.set(true);
}
send(data: any) {
this.ws.send(data);
}
}
export const connection: Writable<WebsocketConnection | null> = writable(null);
export const list: Writable<{ name: string, count: number }[] | null> = writable(null);
export const listLoading = writable(true);
export const lastError: Writable<string> = writable("");
export const room: Writable<{ name: string, host: string } | null> = writable(null);
export const players: Writable<Map<string, ConnectedClient>> = writable(new Map);
export const messages: Writable<{ author: string, content: string }[]> = writable([]);
export const gameData: Writable<{ score: number, lives: number, lastScoreChange: number }|null> = writable(null);

110
client/src/lib/game.svelte Normal file
View file

@ -0,0 +1,110 @@
<script lang="ts">
var classes = [
'top left',
'top middle',
'top right',
'middle left',
'middle-middle',
'middle right',
'bottom left',
'bottom middle',
'bottom right'
];
var containers: HTMLDivElement[] = new Array(9);
var hoveredPiece: null | { i: number, j: number } = null;
var highlightedContainer: null | number = null;
$: highlightedContainer = hoveredPiece?.j ?? null;
var currentContainer: number = 4;
var moves = [
{ p: 1, i: 4, j: 1 },
{ p: 2, i: 1, j: 2 },
{ p: 1, i: 2, j: 4 }
];
</script>
<main class="flex">
<div class="board">
{#each classes as className, i}
<div bind:this={containers[i]} class:current={currentContainer === i} class:highlighted={highlightedContainer === i} class="squares-container {className}">
{#each (new Array(9)) as _, j}
<div class="square" on:click={() => console.log(i, j)} on:mouseover={() => hoveredPiece = { i, j }} on:mouseleave={() => { if(hoveredPiece?.i == i && hoveredPiece.j == j) hoveredPiece = null; }}>
{#if moves.find(move => move.i == i && move.j == j)}
{#if moves.find(move => move.i == i && move.j == j)?.p == 1}
<svg width="16" height="16">
<line x1="0" y1="0" x2="100%" y2="100%" stroke="black" stroke-width="2" />
<line x1="100%" y1="0" x2="0" y2="100%" stroke="black" stroke-width="2" />
</svg>
{:else}
<svg width="16" height="16">
<circle cx="50%" cy="50%" r="7" stroke="black" stroke-width="2" fill="none" />
</svg>
{/if}
{/if}
</div>
{/each}
</div>
{/each}
</div>
<div class="moves">
{#each moves as move}
<div class="move">{move.p == 1 ? "X" : "O"} B{move.i} #{move.j}</div>
{/each}
</div>
</main>
<div class="chat">
</div>
<style>
.moves {
@apply p-4 font-mono;
}
.board {
@apply grid grid-cols-3 grid-rows-3 gap-10 w-max h-max m-auto my-5;
}
.squares-container {
@apply grid grid-cols-3 grid-rows-3 gap-5 w-max h-max;
}
.square {
@apply border-black border-solid border w-6 h-6 p-4 cursor-pointer flex items-center justify-center;
}
.square svg {
@apply w-full h-full;
}
.square:hover {
@apply bg-black/20;
}
.highlighted {
@apply bg-red-700/20;
}
.current {
@apply bg-green-700/20;
}
.highlighted.current {
@apply bg-black/20;
}
.top.left, .middle-middle, .bottom.middle, .bottom.right, .middle.right, .top.left .square:first-child, .middle-middle .square:first-child, .bottom.middle .square:first-child, .bottom.right .square:first-child, .middle.right .square:first-child {
@apply rounded-tl-2xl;
}
.top.right, .middle-middle, .bottom.left, .middle.left, .bottom.middle, .top.right .square:nth-child(3), .middle-middle .square:nth-child(3), .bottom.left .square:nth-child(3), .middle.left .square:nth-child(3), .bottom.middle .square:nth-child(3) {
@apply rounded-tr-2xl;
}
.top.right, .top.middle, .middle-middle, .bottom.left, .middle.right, .top.right .square:nth-child(7), .top.middle .square:nth-child(7), .middle-middle .square:nth-child(7), .bottom.left .square:nth-child(7), .middle.right .square:nth-child(7) {
@apply rounded-bl-2xl;
}
.top.left, .top.middle, .middle-middle, .bottom.right, .middle.left, .top.left .square:last-child, .top.middle .square:last-child, .middle-middle .square:last-child, .bottom.right .square:last-child, .middle.left .square:last-child {
@apply rounded-br-2xl;
}
</style>

View file

@ -1,30 +0,0 @@
<script>
export var disabled = false;
</script>
<button on:click {disabled}><slot /></button>
<style>
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;
}
button:disabled {
background-color: #85E65C;
color: #5f3172;
text-decoration: line-through;
cursor: not-allowed;
}
</style>

View file

@ -1,105 +0,0 @@
<script>
const radius = 10;
export const color = 'rgb(161, 76, 76)';
export const showfaces = false;
export const duration = "0.5s"
</script>
<div class="dice" style="animation-duration: {duration}">
<svg width="100" height="100" class="front" style="background-color: {color}">
{#if showfaces}
<circle cx="50" cy="50" r={radius} fill="black" />
{/if}
</svg>
<svg width="100" height="100" class="top" style="background-color: {color}">
{#if showfaces}
<circle cx="33" cy="33" r={radius} fill="black" />
<circle cx="66" cy="66" r={radius} fill="black" />
{/if}
</svg>
<svg width="100" height="100" class="left" style="background-color: {color}">
{#if showfaces}
<circle cx="30" cy="30" r={radius} fill="black" />
<circle cx="50" cy="50" r={radius} fill="black" />
<circle cx="70" cy="70" r={radius} fill="black" />
{/if}
</svg>
<svg width="100" height="100" class="back" style="background-color: {color}">
{#if showfaces}
<circle cx="25" cy="33" r={radius} fill="black" />
<circle cx="50" cy="33" r={radius} fill="black" />
<circle cx="75" cy="33" r={radius} fill="black" />
<circle cx="25" cy="66" r={radius} fill="black" />
<circle cx="50" cy="66" r={radius} fill="black" />
<circle cx="75" cy="66" r={radius} fill="black" />
{/if}
</svg>
<svg width="100" height="100" class="right" style="background-color: {color}">
{#if showfaces}
<circle cx="33" cy="33" r={radius} fill="black" />
<circle cx="33" cy="66" r={radius} fill="black" />
<circle cx="66" cy="33" r={radius} fill="black" />
<circle cx="66" cy="66" r={radius} fill="black" />
{/if}
</svg>
<svg width="100" height="100" class="bottom" style="background-color: {color}">
{#if showfaces}
<circle cx="30" cy="30" r={radius} fill="black" />
<circle cx="30" cy="70" r={radius} fill="black" />
<circle cx="70" cy="30" r={radius} fill="black" />
<circle cx="70" cy="70" r={radius} fill="black" />
<circle cx="50" cy="50" r={radius} fill="black" />
{/if}
</svg>
</div>
<style>
svg {
border-radius: 5px;
border: 1px solid black;
box-shadow: 0 0 2px black;
position: absolute;
backface-visibility: visible;
}
.front {
transform: translateZ(50px);
}
.top {
transform: rotateX(90deg) translateZ(50px);
}
.bottom {
transform: rotateX(-90deg) translateZ(50px);
}
.left {
transform: rotateY(-90deg) translateZ(50px);
}
.right {
transform: rotateY(90deg) translateZ(50px);
}
.back {
transform: rotateY(180deg) translateZ(50px);
}
.dice {
width: 100px;
height: 100px;
position: relative;
transform: rotate3d(1, 0, 0, 0deg);
transform-style: preserve-3d;
box-sizing: border-box;
animation: jump 0.5s;
animation-fill-mode: both;
}
@keyframes jump {
0% {
transform: rotate3d(1, 0, 0, 0deg) scale(1);
animation-timing-function: ease-in;
}
60% {
transform: rotate3d(1, 0, 0, 90deg) scale(1.4);
animation-timing-function: ease-out;
}
100% {
transform: rotate3d(1, 0, 0, 180deg) scale(1);
}
}
</style>

View file

@ -1,104 +0,0 @@
<script>
const radius = 10;
</script>
<div class="container">
<div class="dice">
<svg width="100" height="100" class="front">
<circle cx="50" cy="50" r={radius} fill="black" />
</svg>
<svg width="100" height="100" class="top">
<circle cx="33" cy="33" r={radius} fill="black" />
<circle cx="66" cy="66" r={radius} fill="black" />
</svg>
<svg width="100" height="100" class="left">
<circle cx="30" cy="30" r={radius} fill="black" />
<circle cx="50" cy="50" r={radius} fill="black" />
<circle cx="70" cy="70" r={radius} fill="black" />
</svg>
<svg width="100" height="100" class="back">
<circle cx="25" cy="33" r={radius} fill="black" />
<circle cx="50" cy="33" r={radius} fill="black" />
<circle cx="75" cy="33" r={radius} fill="black" />
<circle cx="25" cy="66" r={radius} fill="black" />
<circle cx="50" cy="66" r={radius} fill="black" />
<circle cx="75" cy="66" r={radius} fill="black" />
</svg>
<svg width="100" height="100" class="right">
<circle cx="33" cy="33" r={radius} fill="black" />
<circle cx="33" cy="66" r={radius} fill="black" />
<circle cx="66" cy="33" r={radius} fill="black" />
<circle cx="66" cy="66" r={radius} fill="black" />
</svg>
<svg width="100" height="100" class="bottom">
<circle cx="30" cy="30" r={radius} fill="black" />
<circle cx="30" cy="70" r={radius} fill="black" />
<circle cx="70" cy="30" r={radius} fill="black" />
<circle cx="70" cy="70" r={radius} fill="black" />
<circle cx="50" cy="50" r={radius} fill="black" />
</svg>
</div>
</div>
<style>
svg {
border-radius: 5px;
border: 1px solid black;
box-shadow: 0 0 2px black;
position: absolute;
backface-visibility: visible;
}
.front {
transform: translateZ(50px);
background-color: rgba(255, 0, 0, 0.7);
}
.top {
transform: rotateX(90deg) translateZ(50px);
background-color: rgba(255, 255, 0, 0.7);
}
.bottom {
transform: rotateX(-90deg) translateZ(50px);
background-color: rgba(0, 255, 0, 0.7);
}
.left {
transform: rotateY(-90deg) translateZ(50px);
background-color: rgba(255, 0, 255, 0.7);
}
.right {
transform: rotateY(90deg) translateZ(50px);
background-color: rgba(0, 0, 255, 0.7);
}
.back {
transform: rotateY(180deg) translateZ(50px);
background-color: rgba(0, 0, 0, 0.7);
}
.dice {
width: 100px;
height: 100px;
position: relative;
transform: rotate3d(1, 0, 0, 0deg);
transform-style: preserve-3d;
box-sizing: border-box;
animation: dice 1.2s 0.5s ease-in-out;
animation-fill-mode: both;
}
@keyframes dice {
0% {
transform: rotate3d(0, 1, 0, 90deg); /* 3 */
}
50% {
transform: rotate3d(1, 0, 0, -90deg); /* 2 */
}
100% {
transform: rotate3d(1, 0, 0, 0deg); /* 1 */
}
}
.container {
width: 100vw;
height: 100vh;
background-color: #85E65C;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View file

@ -1,16 +0,0 @@
<script>
import { gameData } from '$lib/Websocket';
import GameOverlay from './gameOverlay.svelte';
import Scene from './scene.svelte';
import TimedPrestartScreen from './timedPrestartScreen.svelte';
import Waiting from './waiting.svelte';
</script>
{#if !$gameData}
<Waiting />
{:else}
<TimedPrestartScreen />
<GameOverlay>
<Scene />
</GameOverlay>
{/if}

View file

@ -1,95 +0,0 @@
<script lang="ts">
import { connection, gameData, players } from "$lib/Websocket";
import { flip } from "svelte/animate";
let scoreboard: { name: string, score: number, lives: number, lastScoreChange: number }[] = [];
$: {
scoreboard = [...$players.values(), { name: $connection!.name, score: $gameData!.score, lives: $gameData!.lives, lastScoreChange: $gameData!.lastScoreChange }].sort((a, b) => (b.score - a.score) || (b.lives - a.lives) || (a.lastScoreChange - b.lastScoreChange) || a.name.localeCompare(b.name));
}
</script>
<div class="container">
<div class="left">
<div class="hearts">
{#each [...Array($gameData?.lives).keys()] as i}
<img src="/assets/heart.png" alt="">
{/each}
</div>
<ul>
{#each scoreboard as player (player.name)}
<li animate:flip class:dead-player={!player.lives}>
{player.name}
<span class="score">{player.score}</span>
{#if !player.lives}
<div class="dead">DEAD</div>
{/if}
</li>
{/each}
</ul>
</div>
<main>
<slot />
</main>
<div class="right">
</div>
</div>
<style>
.hearts img {
width: 20px;
height: 20px;
margin: 0 5px;
}
.hearts {
height: 30px;
}
.container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 100vh;
width: 100vw;
background-color: #85e65c;
}
main {
max-width: calc(100vw - 310px);
border-left: 10px solid #bd5ce6;
}
.left {
flex-grow: 1;
width: 300px;
height: calc(100% - 10px);
margin: 5px;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem;
border-bottom: 1px solid #ccc;
}
li:last-child {
border-bottom: none;
}
li.dead-player {
color: #ccc;
}
.dead {
font-weight: bold;
font-size: 1.2rem;
margin-left: 1rem;
}
.score {
font-size: 1.5em;
font-weight: bold;
background-color: #bd5ce6;
color: #85e65c;
}
</style>

View file

@ -1,51 +0,0 @@
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);
}
var htmlcanvas: HTMLCanvasElement;
var game: Game;
var gs: GameScene | null = null;
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: "#85e65c",
banner: false
});
}
export function stop() {
game.destroy(false);
gs = null;
}

View file

@ -1,33 +0,0 @@
<script lang="ts">
import { connection, gameData } from "$lib/Websocket";
import { onMount } from "svelte";
import { setCanvas, resize, stop } from "./init";
var canvas: HTMLCanvasElement;
onMount(() => {
console.log("Started");
setCanvas(canvas);
return () => {
console.log("Stopped");
stop();
}
});
function increaseScore() {
$connection!.setScore($gameData!.score + 1);
}
function decreaseLives() {
$connection!.setLives($gameData!.lives - 1);
}
function keydown(e: KeyboardEvent) {
if(e.key === "KeyT") {
increaseScore();
}
}
</script>
<svelte:window on:resize={resize} on:keydown={keydown}/>
<canvas bind:this={canvas} on:click={increaseScore} on:contextmenu={decreaseLives} />

View file

@ -1,28 +0,0 @@
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

@ -1,17 +0,0 @@
<script>
import { onMount } from "svelte";
import PrestartScreen from "../components/prestartScreen.svelte";
let visible = true;
onMount(() => {
let i = setTimeout(() => {
visible = false;
}, 2000);
return () => clearTimeout(i);
});
</script>
{#if visible}
<PrestartScreen />
{/if}

View file

@ -1,170 +0,0 @@
<script lang="ts">
import { connection, messages, players, room } from '$lib/Websocket';
import Button from '../components/button.svelte';
function startGame() {
$connection!.startGame();
}
let content = "";
function sendMessage() {
if(!content.length || content.length > 512) return;
$connection!.sendMessage(content);
content = "";
}
$: if($messages.length > 512) {
$messages = $messages.slice(-512);
}
</script>
<div class="container">
<div class="playerlist">
<ul>
<li>
{$connection?.name}
{#if $room?.host === $connection?.name}
<span class="host">Host</span>
{/if}
<span class="state">YOU</span>
</li>
{#each [...$players.values()] as player (player.name)}
<li>
{player.name}
{#if $room?.host === player.name}
<span class="host">Host</span>
{/if}
<span class="state state-{player.readyState}"
>{['joined', 'connecting', 'connecting...', 'ready', 'reconnecting'][
player.readyState
]}</span
>
{#if player.pings && player.pings.length > 0}
<span class="pings">{Math.floor(player.pings.reduce((a, b) => a + b, 0) / player.pings.length * 10) / 10}ms</span>
{/if}
</li>
{/each}
</ul>
</div>
<main>
<div class="text">
{#each $messages as message}
<div class="message">
{#if message.author !== " SYS "}
<span class="name">{message.author}</span>:
{/if}
<span class="content">{message.content}</span>
</div>
{/each}
</div>
<div class="bottom">
<form action="/" on:submit|preventDefault={sendMessage}>
<input type="text" placeholder="Chat.." bind:value={content} on:submit|preventDefault={sendMessage}>
</form>
<Button on:click={() => sendMessage()}>Send</Button>
{#if $room?.host === $connection?.name && $players.size > 0}
<Button on:click={() => startGame()}>START GAME</Button>
{:else}
<Button disabled>WAIT TO START</Button>
{/if}
</div>
</main>
</div>
<style>
.pings {
width: 3em;
text-align: right;
display: inline-block;
}
.container {
width: 100vw;
height: 100vh;
background-color: #85e65c;
color: #bd5ce6;
display: flex;
flex-direction: row;
align-items: center;
}
.playerlist {
width: 20em;
height: 100%;
background-color: #85e65c;
color: #bd5ce6;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-right: 10px solid #bd5ce6;
}
.state {
font-size: 0.8em;
margin-left: 0.5em;
background-color: #bd5ce6;
color: #85e65c;
border-radius: 0.25em;
padding: 0.3em;
}
.state.state-0 {
background-color: red;
}
.state.state-3 {
background-color: transparent;
color: #000;
}
.state.state-4 {
background-color: #ff0000;
color: #000;
}
ul {
list-style: none;
padding: 0;
}
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
height: calc(100vh - 40px);
}
.text {
flex-grow: 1;
width: calc(100% - 2rem);
margin: 1em;
}
.bottom {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
flex-grow: 0;
height: 2em;
width: 100%;
}
form {
flex-grow: 1;
}
input {
width: 100%;
background-color: #85e65c;
color: #bd5ce6;
font-size: 1.5em;
font-family: inherit;
padding: 0.5em;
border: 10px solid #bd5ce6;
border-left: none;
border-right: none;
}
.text {
overflow: auto;
}
.text .message {
height: 1.5em;
}
.text .name {
font-weight: bold;
background-color: #bd5ce6;
color: #85e65c;
padding: 0.2em;
border-radius: 0.25em;
}
</style>

View file

@ -1,144 +0,0 @@
<script lang="ts">
import { connection, lastError, list, listLoading } from "$lib/Websocket";
import { onMount } from "svelte";
import Button from "../components/button.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);
}
function connect(game: { name: string, count: number }) {
$connection!.join(game.name);
}
</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" on:click={() => connect(game)}>
<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;
}
input {
font-family: inherit;
}
</style>

View file

@ -1,61 +0,0 @@
<script>
import { connection, lastError, WebsocketConnection } from "$lib/Websocket";
import Button from "../components/button.svelte";
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;
}
</style>

167
client/src/lib/websocket.ts Normal file
View file

@ -0,0 +1,167 @@
import { writable, type Writable } from "svelte/store";
export class WebsocketConnection extends EventTarget {
ws: WebSocket;
roomName: string | null = null;
roomHost: string | null = null;
players: Set<string>;
constructor(public name: string) {
super();
// @ts-ignore Initialized in the next function call
this.ws = null;
this.players = new Set([name]);
this.connect();
}
connect() {
const host = location.hostname.includes("danbulant.eu") ? "wss://multidie.danbulant.cloud" : "ws://" + location.hostname + ":8080";
this.ws = new WebSocket(host + "/?name=" + encodeURIComponent(this.name));
this.ws.addEventListener("open", (e) => {
console.log("WS ready");
this.refreshList();
});
this.ws.addEventListener("close", (e) => {
console.log("WS closed");
this.addError(e.reason || "Connection closed");
connection.set(null);
room.set(null);
list.set(null);
});
this.ws.addEventListener("error", (e) => {
console.error("WS error");
this.addError("Connection error");
connection.set(null);
room.set(null);
list.set(null);
});
this.ws.addEventListener("message", (e) => {
const msg = JSON.parse(e.data);
console.log(msg);
switch (msg.t) {
case "join": {
messages.update(t => { t.push({ type: "system", content: `${msg.client} joined`});return t})
this.players.add(msg.client);
break;
}
case "joined": {
const clients = msg.clients;
this.players = new Set(clients);
messages.set([{
type: "system", content: `${msg.client} joined`
}]);
this.roomName = msg.name;
this.roomHost = msg.host;
room.set({
name: msg.name,
host: msg.host
});
break;
}
case "create": {
this.roomName = msg.name;
this.roomHost = this.name;
room.set({
name: msg.name,
host: this.name
});
this.addMessage({ type: "system", content: `${msg.name} created the room`});
break;
}
case "leave": {
this.addMessage({ type: "system", content: `${msg.client} left`});
if(this.players.size == 0) {
gameData.set(null);
}
this.players.delete(msg.client);
break;
}
case "host": {
this.roomHost = msg.host;
room.update(t => { t!.host = this.roomHost!; return t });
this.addMessage({ type: "system", content: `${msg.host} is now host`});
break;
}
case "left": {
console.log("Left room successfully");
this.roomName = null;
room.set(null);
messages.set([]);
this.players = new Set([this.name]);
break;
}
case "list": {
list.set(msg.rooms);
listLoading.set(false);
break;
}
case "error": {
console.error(msg.e);
this.addError(msg.e);
break;
}
}
});
}
addMessage(message: ErrorMessage | UserMessage | SystemMessage) {
messages.update(t => { t.push(message); return t });
}
addError(data: string) {
messages.update(t => { t.push({ type: "error", error: data }); return t });
}
sendMessage(msg: string) {
if (!this.roomName) return console.log("Not in a room");
this.broadcast({ t: "msg", d: msg });
messages.update(t => { t.push({ type: "user", author: this.name, content: msg }); return t });
}
broadcast(data: any) {
if (!this.roomName) return console.log("Not in a room");
this.ws.send(JSON.stringify({ t: "broadcast", d: data }));
}
createGame(name: string) {
this.ws.send(JSON.stringify({ t: "create", name: name }));
}
startGame() {
if (!this.roomName) return console.log("Not in a room");
this.broadcast({ t: "start" });
gameData.set({ log: [] });
}
join(name: string) {
this.ws.send(JSON.stringify({ t: "join", name: name }));
}
refreshList() {
this.ws.send(JSON.stringify({ t: "list" }));
listLoading.set(true);
}
send(data: any) {
this.ws.send(data);
}
}
interface ErrorMessage {
type: "error",
error: string
}
interface UserMessage {
type: "user",
author: string,
content: string
}
interface SystemMessage {
type: "system",
content: string
}
export const connection: Writable<WebsocketConnection | null> = writable(null);
export const list: Writable<{ name: string, count: number }[] | null> = writable(null);
export const listLoading = writable(true);
export const room: Writable<{ name: string, host: string } | null> = writable(null);
export const messages: Writable<(UserMessage | ErrorMessage | SystemMessage)[]> = writable([]);
export const gameData: Writable<{ log: { p: string, i: number, j: number }[] }|null> = writable(null);

View file

@ -0,0 +1,5 @@
<script>
import "uno.css";
</script>
<slot />

View file

@ -0,0 +1,6 @@
<script>
import Game from "$lib/game.svelte";
</script>
<Game />

View file

@ -1,8 +1,22 @@
import { sveltekit } from '@sveltejs/kit/vite';
import UnoCSS from 'unocss/vite';
import { presetUno } from 'unocss';
import { extractorSvelte } from '@unocss/core';
import transformerDirectives from '@unocss/transformer-directives';
import transformerVariantGroup from '@unocss/transformer-variant-group';
/** @type {import('vite').UserConfig} */
const config = {
plugins: [sveltekit()]
plugins: [UnoCSS({
presets: [
presetUno()
],
extractors: [extractorSvelte],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
}), sveltekit()]
};
export default config;

File diff suppressed because it is too large Load diff

View file

@ -217,8 +217,7 @@ require("uWebSockets.js")
);
break;
}
case "cand":
case "desc": {
case "broadcast": {
if (!client) return ws.end(0, "missing_client");
const room = client.room;
if (!room)
@ -228,41 +227,28 @@ require("uWebSockets.js")
e: "room_not_found",
})
);
if (!room.clients.includes(client))
return ws.send(
JSON.stringify({ t: "error", e: "not_in_room" })
);
const targetClient = room.clients.find(
(t) => t.name === data.target
);
if (!targetClient)
return ws.send(
room.clients.forEach((client) => {
client.connection.send(
JSON.stringify({
t: "error",
e: "target_not_found",
t: "broadcast",
client: client.name,
data: data.d,
})
);
if (!room.clients.includes(targetClient))
return ws.send(
JSON.stringify({
t: "error",
e: "target_not_in_room",
})
);
targetClient.connection.send(
JSON.stringify({
t: data.t,
source: client.name,
d: data.d,
})
);
});
}
case "list": {
ws.send(
JSON.stringify({
t: "list",
rooms: [...rooms.values()]
.filter((t) => t.clients.length < 5)
.filter((t) => t.clients.length < 2)
.map((t) => ({
name: t.name,
host: t.host.name,