mirror of
https://github.com/danbulant/slightlyComplicatedTicTacToe
synced 2026-06-21 15:52:01 +00:00
initial wip
This commit is contained in:
parent
62fa17b953
commit
21a3ae9ea3
22 changed files with 1182 additions and 1974 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
110
client/src/lib/game.svelte
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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} />
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
167
client/src/lib/websocket.ts
Normal 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);
|
||||
5
client/src/routes/+layout.svelte
Normal file
5
client/src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import "uno.css";
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
6
client/src/routes/test/+page.svelte
Normal file
6
client/src/routes/test/+page.svelte
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<script>
|
||||
import Game from "$lib/game.svelte";
|
||||
|
||||
</script>
|
||||
|
||||
<Game />
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
1580
pnpm-lock.yaml
1580
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue