diff --git a/.parcelrc b/.parcelrc
new file mode 100644
index 0000000..55a9ca4
--- /dev/null
+++ b/.parcelrc
@@ -0,0 +1,4 @@
+{
+ "extends": ["@parcel/config-default"],
+ "reporters": ["...", "parcel-reporter-static-files-copy"]
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 9a3462f..920b6fb 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,10 @@
"private": true,
"scripts": {
"build": "parcel build --dist-dir dist src/html/index.html",
- "serve": "parcel src/html/index.html"
+ "serve": "parcel src/html/index.html",
+ "clean": "run-script-os",
+ "clean:darwin:linux": "rm -rf dist .parcel-cache",
+ "clean:win32": "rd /s /q dist .parcel-cache"
},
"author": "Elijah R",
"license": "GPL-3.0",
@@ -20,6 +23,8 @@
},
"devDependencies": {
"parcel": "^2.11.0",
+ "parcel-reporter-static-files-copy": "^1.5.3",
+ "run-script-os": "^1.1.6",
"typescript": "^5.3.3"
}
}
diff --git a/src/css/style.css b/src/css/style.css
index 6eb1b37..e2d944b 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -87,10 +87,26 @@
display: none;
}
-.user-admin {
+tr.user-admin > td {
color: #FF0000 !important;
}
-.user-moderator {
+tr.user-moderator > td {
color: #00FF00 !important;
+}
+
+tr.user-turn > td {
+ background-color: #cfe2ff !important;
+}
+
+tr.user-turn > td:hover, tr.user-turn > td:active {
+ background-color: #bacbe6 !important;
+}
+
+tr.user-waiting > td {
+ background-color: #fff3cd !important;
+}
+
+.tr.user-waiting > td:hover, .tr.user-waiting > td:active {
+ background-color: #ece1be;
}
\ No newline at end of file
diff --git a/src/html/index.html b/src/html/index.html
index a4539ec..df5cdd9 100644
--- a/src/html/index.html
+++ b/src/html/index.html
@@ -129,7 +129,7 @@
-
+
diff --git a/src/js/keyboard.js b/src/js/keyboard.js
new file mode 100644
index 0000000..6425bc4
--- /dev/null
+++ b/src/js/keyboard.js
@@ -0,0 +1,282 @@
+// Pulled a bunch of functions out of the guac source code to get a keysym
+// and then a wrapper
+// shitty but it works so /shrug
+// THIS SUCKS SO BAD AND I HATE IT PLEASE REWRITE ALL OF THIS
+
+export default function GetKeysym(keyCode, keyIdentifier, key, location) {
+ var keysym = keysym_from_key_identifier(key, location)
+ || keysym_from_keycode(keyCode, location);
+
+ if (!keysym && key_identifier_sane(keyCode, keyIdentifier))
+ keysym = keysym_from_key_identifier(keyIdentifier, location);
+
+ return keysym;
+}
+
+
+function keysym_from_key_identifier(identifier, location) {
+
+ if (!identifier)
+ return null;
+
+ var typedCharacter;
+
+ // If identifier is U+xxxx, decode Unicode character
+ var unicodePrefixLocation = identifier.indexOf("U+");
+ if (unicodePrefixLocation >= 0) {
+ var hex = identifier.substring(unicodePrefixLocation+2);
+ typedCharacter = String.fromCharCode(parseInt(hex, 16));
+ }
+
+ // If single character, use that as typed character
+ else if (identifier.length === 1)
+ typedCharacter = identifier;
+
+ // Otherwise, look up corresponding keysym
+ else
+ return get_keysym(keyidentifier_keysym[identifier], location);
+
+ // Get codepoint
+ var codepoint = typedCharacter.charCodeAt(0);
+ return keysym_from_charcode(codepoint);
+
+}
+
+function get_keysym(keysyms, location) {
+
+ if (!keysyms)
+ return null;
+
+ return keysyms[location] || keysyms[0];
+}
+
+function keysym_from_charcode(codepoint) {
+
+ // Keysyms for control characters
+ if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
+
+ // Keysyms for ASCII chars
+ if (codepoint >= 0x0000 && codepoint <= 0x00FF)
+ return codepoint;
+
+ // Keysyms for Unicode
+ if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
+ return 0x01000000 | codepoint;
+
+ return null;
+}
+
+
+function isControlCharacter(codepoint) {
+ return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
+}
+
+function keysym_from_keycode(keyCode, location) {
+ return get_keysym(keycodeKeysyms[keyCode], location);
+}
+
+function key_identifier_sane(keyCode, keyIdentifier) {
+
+ // Missing identifier is not sane
+ if (!keyIdentifier)
+ return false;
+
+ // Assume non-Unicode keyIdentifier values are sane
+ var unicodePrefixLocation = keyIdentifier.indexOf("U+");
+ if (unicodePrefixLocation === -1)
+ return true;
+
+ // If the Unicode codepoint isn't identical to the keyCode,
+ // then the identifier is likely correct
+ var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16);
+ if (keyCode !== codepoint)
+ return true;
+
+ // The keyCodes for A-Z and 0-9 are actually identical to their
+ // Unicode codepoints
+ if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57))
+ return true;
+
+ // The keyIdentifier does NOT appear sane
+ return false;
+
+}
+
+var keycodeKeysyms = {
+ 8: [0xFF08], // backspace
+ 9: [0xFF09], // tab
+ 12: [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear / KP 5
+ 13: [0xFF0D], // enter
+ 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift
+ 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl
+ 18: [0xFFE9, 0xFFE9, 0xFE03], // alt
+ 19: [0xFF13], // pause/break
+ 20: [0xFFE5], // caps lock
+ 27: [0xFF1B], // escape
+ 32: [0x0020], // space
+ 33: [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up / KP 9
+ 34: [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down / KP 3
+ 35: [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end / KP 1
+ 36: [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home / KP 7
+ 37: [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow / KP 4
+ 38: [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow / KP 8
+ 39: [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6
+ 40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2
+ 45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0
+ 46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal
+ 91: [0xFFEB], // left window key (hyper_l)
+ 92: [0xFF67], // right window key (menu key?)
+ 93: null, // select key
+ 96: [0xFFB0], // KP 0
+ 97: [0xFFB1], // KP 1
+ 98: [0xFFB2], // KP 2
+ 99: [0xFFB3], // KP 3
+ 100: [0xFFB4], // KP 4
+ 101: [0xFFB5], // KP 5
+ 102: [0xFFB6], // KP 6
+ 103: [0xFFB7], // KP 7
+ 104: [0xFFB8], // KP 8
+ 105: [0xFFB9], // KP 9
+ 106: [0xFFAA], // KP multiply
+ 107: [0xFFAB], // KP add
+ 109: [0xFFAD], // KP subtract
+ 110: [0xFFAE], // KP decimal
+ 111: [0xFFAF], // KP divide
+ 112: [0xFFBE], // f1
+ 113: [0xFFBF], // f2
+ 114: [0xFFC0], // f3
+ 115: [0xFFC1], // f4
+ 116: [0xFFC2], // f5
+ 117: [0xFFC3], // f6
+ 118: [0xFFC4], // f7
+ 119: [0xFFC5], // f8
+ 120: [0xFFC6], // f9
+ 121: [0xFFC7], // f10
+ 122: [0xFFC8], // f11
+ 123: [0xFFC9], // f12
+ 144: [0xFF7F], // num lock
+ 145: [0xFF14], // scroll lock
+ 225: [0xFE03] // altgraph (iso_level3_shift)
+};
+
+var keyidentifier_keysym = {
+ "Again": [0xFF66],
+ "AllCandidates": [0xFF3D],
+ "Alphanumeric": [0xFF30],
+ "Alt": [0xFFE9, 0xFFE9, 0xFE03],
+ "Attn": [0xFD0E],
+ "AltGraph": [0xFE03],
+ "ArrowDown": [0xFF54],
+ "ArrowLeft": [0xFF51],
+ "ArrowRight": [0xFF53],
+ "ArrowUp": [0xFF52],
+ "Backspace": [0xFF08],
+ "CapsLock": [0xFFE5],
+ "Cancel": [0xFF69],
+ "Clear": [0xFF0B],
+ "Convert": [0xFF21],
+ "Copy": [0xFD15],
+ "Crsel": [0xFD1C],
+ "CrSel": [0xFD1C],
+ "CodeInput": [0xFF37],
+ "Compose": [0xFF20],
+ "Control": [0xFFE3, 0xFFE3, 0xFFE4],
+ "ContextMenu": [0xFF67],
+ "DeadGrave": [0xFE50],
+ "DeadAcute": [0xFE51],
+ "DeadCircumflex": [0xFE52],
+ "DeadTilde": [0xFE53],
+ "DeadMacron": [0xFE54],
+ "DeadBreve": [0xFE55],
+ "DeadAboveDot": [0xFE56],
+ "DeadUmlaut": [0xFE57],
+ "DeadAboveRing": [0xFE58],
+ "DeadDoubleacute": [0xFE59],
+ "DeadCaron": [0xFE5A],
+ "DeadCedilla": [0xFE5B],
+ "DeadOgonek": [0xFE5C],
+ "DeadIota": [0xFE5D],
+ "DeadVoicedSound": [0xFE5E],
+ "DeadSemivoicedSound": [0xFE5F],
+ "Delete": [0xFFFF],
+ "Down": [0xFF54],
+ "End": [0xFF57],
+ "Enter": [0xFF0D],
+ "EraseEof": [0xFD06],
+ "Escape": [0xFF1B],
+ "Execute": [0xFF62],
+ "Exsel": [0xFD1D],
+ "ExSel": [0xFD1D],
+ "F1": [0xFFBE],
+ "F2": [0xFFBF],
+ "F3": [0xFFC0],
+ "F4": [0xFFC1],
+ "F5": [0xFFC2],
+ "F6": [0xFFC3],
+ "F7": [0xFFC4],
+ "F8": [0xFFC5],
+ "F9": [0xFFC6],
+ "F10": [0xFFC7],
+ "F11": [0xFFC8],
+ "F12": [0xFFC9],
+ "F13": [0xFFCA],
+ "F14": [0xFFCB],
+ "F15": [0xFFCC],
+ "F16": [0xFFCD],
+ "F17": [0xFFCE],
+ "F18": [0xFFCF],
+ "F19": [0xFFD0],
+ "F20": [0xFFD1],
+ "F21": [0xFFD2],
+ "F22": [0xFFD3],
+ "F23": [0xFFD4],
+ "F24": [0xFFD5],
+ "Find": [0xFF68],
+ "GroupFirst": [0xFE0C],
+ "GroupLast": [0xFE0E],
+ "GroupNext": [0xFE08],
+ "GroupPrevious": [0xFE0A],
+ "FullWidth": null,
+ "HalfWidth": null,
+ "HangulMode": [0xFF31],
+ "Hankaku": [0xFF29],
+ "HanjaMode": [0xFF34],
+ "Help": [0xFF6A],
+ "Hiragana": [0xFF25],
+ "HiraganaKatakana": [0xFF27],
+ "Home": [0xFF50],
+ "Hyper": [0xFFED, 0xFFED, 0xFFEE],
+ "Insert": [0xFF63],
+ "JapaneseHiragana": [0xFF25],
+ "JapaneseKatakana": [0xFF26],
+ "JapaneseRomaji": [0xFF24],
+ "JunjaMode": [0xFF38],
+ "KanaMode": [0xFF2D],
+ "KanjiMode": [0xFF21],
+ "Katakana": [0xFF26],
+ "Left": [0xFF51],
+ "Meta": [0xFFE7, 0xFFE7, 0xFFE8],
+ "ModeChange": [0xFF7E],
+ "NumLock": [0xFF7F],
+ "PageDown": [0xFF56],
+ "PageUp": [0xFF55],
+ "Pause": [0xFF13],
+ "Play": [0xFD16],
+ "PreviousCandidate": [0xFF3E],
+ "PrintScreen": [0xFD1D],
+ "Redo": [0xFF66],
+ "Right": [0xFF53],
+ "RomanCharacters": null,
+ "Scroll": [0xFF14],
+ "Select": [0xFF60],
+ "Separator": [0xFFAC],
+ "Shift": [0xFFE1, 0xFFE1, 0xFFE2],
+ "SingleCandidate": [0xFF3C],
+ "Super": [0xFFEB, 0xFFEB, 0xFFEC],
+ "Tab": [0xFF09],
+ "Up": [0xFF52],
+ "Undo": [0xFF65],
+ "Win": [0xFFEB],
+ "Zenkaku": [0xFF28],
+ "ZenkakuHankaku": [0xFF2A]
+};
\ No newline at end of file
diff --git a/src/ts/main.ts b/src/ts/main.ts
index 57ffaf2..8a21656 100644
--- a/src/ts/main.ts
+++ b/src/ts/main.ts
@@ -3,6 +3,7 @@ import VM from "./protocol/VM.js";
import { Config } from "../../Config.js";
import { Rank } from "./protocol/Permissions.js";
import { User } from "./protocol/User.js";
+import TurnStatus from "./protocol/TurnStatus.js";
// Elements
const w = window as any;
@@ -16,7 +17,14 @@ const elements = {
userlist: document.getElementById('userlist') as HTMLTableSectionElement,
onlineusercount: document.getElementById("onlineusercount") as HTMLSpanElement,
username: document.getElementById("username") as HTMLSpanElement,
+ chatinput: document.getElementById("chat-input") as HTMLInputElement,
+ sendChatBtn: document.getElementById("sendChatBtn") as HTMLButtonElement,
+ changeUsernameBtn: document.getElementById("changeUsernameBtn") as HTMLButtonElement,
+ turnBtnText: document.getElementById("turnBtnText") as HTMLSpanElement,
+ turnstatus: document.getElementById("turnstatus") as HTMLParagraphElement,
}
+var expectedClose = false;
+var turn = -1;
// Listed VMs
const vms : VM[] = [];
const cards : HTMLDivElement[] = [];
@@ -61,22 +69,31 @@ function openVM(vm : VM) {
return new Promise(async (res, rej) => {
// If there's an active VM it must be closed before opening another
if (VM !== null) return;
+ expectedClose = false;
// Set hash
location.hash = vm.id;
// Create the client
VM = new CollabVMClient(vm.url);
// Register event listeners
- VM!.on('chat', (username, message) => chatMessage(username, message));
- VM!.on('adduser', (user) => addUser(user));
- VM!.on('remuser', (user) => remUser(user));
- VM!.on('rename', (oldname, newname, selfrename) => userRenamed(oldname, newname, selfrename));
- VM!.on('renamestatus', (status) => {
+ // An array to keep track of all listeners, and remove them when the VM is closed. Might not be necessary, but it's good practice.
+ var listeners : (() => void)[] = [];
+ listeners.push(VM!.on('chat', (username, message) => chatMessage(username, message)));
+ listeners.push(VM!.on('adduser', (user) => addUser(user)));
+ listeners.push(VM!.on('remuser', (user) => remUser(user)));
+ listeners.push(VM!.on('rename', (oldname, newname, selfrename) => userRenamed(oldname, newname, selfrename)));
+ listeners.push(VM!.on('renamestatus', (status) => {
switch (status) {
case 'taken': alert("That username is already taken"); break;
case 'invalid': alert("Usernames can contain only numbers, letters, spaces, dashes, underscores, and dots, and it must be between 3 and 20 characters."); break;
case 'blacklisted': alert("That username has been blacklisted."); break;
}
- });
+ }));
+ listeners.push(VM!.on('turn', status => turnUpdate(status)));
+ listeners.push(VM!.on('close', () => {
+ if (!expectedClose) alert("You have been disconnected from the server");
+ for (var l of listeners) l();
+ closeVM();
+ }));
// Wait for the client to open
await new Promise(res => VM!.on('open', () => res()));
// Connect to node
@@ -99,6 +116,7 @@ function openVM(vm : VM) {
function closeVM() {
if (VM === null) return;
+ expectedClose = true;
// Close the VM
VM.close();
VM = null;
@@ -118,7 +136,7 @@ function loadList() {
p.push(multicollab(url));
}
await Promise.all(p);
- var v = vms.find(v => v.id === window.location.hash.slice(1));
+ var v = vms.find(v => v.id === window.location.hash.substring(1));
if (v !== undefined) openVM(v);
res();
});
@@ -132,6 +150,21 @@ function sortVMList() {
cards.forEach((c) => elements.vmlist.children[0].appendChild(c));
}
+function sortUserList() {
+ const users = Array.prototype.slice.call(elements.userlist.children);
+ users.sort((a, b) => {
+ if (parseInt(a.getAttribute("data-cvm-turn")) === parseInt(b.getAttribute("data-cvm-turn"))) return 0;
+ if (parseInt(a.getAttribute("data-cvm-turn")) === -1) return 1;
+ if (parseInt(b.getAttribute("data-cvm-turn")) === -1) return -1;
+ if (parseInt(a.getAttribute("data-cvm-turn")) < parseInt(b.getAttribute("data-cvm-turn"))) return -1;
+ else return 1;
+ });
+ for (const user of users) {
+ elements.userlist.removeChild(user);
+ elements.userlist.appendChild(user);
+ }
+}
+
function chatMessage(username : string, message : string) {
var tr = document.createElement('tr');
var td = document.createElement('td');
@@ -176,17 +209,18 @@ function addUser(user : User) {
var olduser = Array.prototype.slice.call(elements.userlist.children).find((u : HTMLTableRowElement) => u.children[0].innerHTML === user.username);
if (olduser !== undefined) elements.userlist.removeChild(olduser);
var tr = document.createElement('tr');
+ tr.setAttribute("data-cvm-turn", "-1");
var td = document.createElement('td');
td.innerHTML = user.username;
switch (user.rank) {
case Rank.Admin:
- td.classList.add("user-admin");
+ tr.classList.add("user-admin");
break;
case Rank.Moderator:
- td.classList.add("user-moderator");
+ tr.classList.add("user-moderator");
break;
case Rank.Unregistered:
- td.classList.add("user-unregistered");
+ tr.classList.add("user-unregistered");
break;
}
tr.appendChild(td);
@@ -212,9 +246,56 @@ function userRenamed(oldname : string, newname : string, selfrename : boolean) {
}
}
+function turnUpdate(status : TurnStatus) {
+ const users = Array.prototype.slice.call(elements.userlist.children);
+ // Clear all turn data
+ turn = -1;
+ for (const user of users) {
+ user.classList.remove("user-turn", "user-waiting");
+ user.setAttribute("data-cvm-turn", "-1");
+ }
+ elements.turnBtnText.innerHTML = "Take Turn";
+ if (status.user !== null) {
+ var el = users.find((e : HTMLTableRowElement) => e.children[0].innerHTML === status.user!.username);
+ el!.classList.add("user-turn");
+ el!.setAttribute("data-cvm-turn", "0");
+ }
+ for (const user of status.queue) {
+ var el = users.find((e : HTMLTableRowElement) => e.children[0].innerHTML === user.username);
+ el!.classList.add("user-waiting");
+ el.setAttribute("data-cvm-turn", status.queue.indexOf(user))
+ }
+ if (status.user?.username === w.username) {
+ turn = 0;
+ elements.turnBtnText.innerHTML = "End Turn";
+ }
+ if (status.queue.some(u => u.username === w.username)) {
+ turn = status.queue.findIndex(u => u.username === w.username) + 1;
+ elements.turnBtnText.innerHTML = "End Turn";
+ }
+ sortUserList();
+}
+
+function sendChat() {
+ if (VM === null) return;
+ VM.chat(elements.chatinput.value);
+ elements.chatinput.value = "";
+}
+
// Bind list buttons
elements.homeBtn.addEventListener('click', () => closeVM());
+// Bind VM view buttons
+elements.sendChatBtn.addEventListener('click', sendChat);
+elements.chatinput.addEventListener('keypress', (e) => {
+ if (e.key === "Enter") sendChat();
+});
+elements.changeUsernameBtn.addEventListener('click', () => {
+ var newname = prompt("Enter new username, or leave blank to be assigned a guest username", w.username);
+ if (newname === w.username) return;
+ VM?.rename(newname);
+})
+
// Public API
w.collabvm = {
openVM: openVM,
diff --git a/src/ts/protocol/CollabVMClient.ts b/src/ts/protocol/CollabVMClient.ts
index 195b930..fdacc0d 100644
--- a/src/ts/protocol/CollabVMClient.ts
+++ b/src/ts/protocol/CollabVMClient.ts
@@ -3,6 +3,7 @@ import * as Guacutils from './Guacutils.js';
import VM from "./VM.js";
import { User } from "./User.js";
import { Rank } from "./Permissions.js";
+import TurnStatus from "./TurnStatus.js";
export default class CollabVMClient {
// Fields
@@ -35,6 +36,7 @@ export default class CollabVMClient {
// Add the event listeners
this.socket.addEventListener('open', () => this.onOpen());
this.socket.addEventListener('message', (event) => this.onMessage(event));
+ this.socket.addEventListener('close', () => this.publicEmitter.emit('close'));
}
// Fires when the WebSocket connection is opened
@@ -138,6 +140,37 @@ export default class CollabVMClient {
this.publicEmitter.emit('rename', oldusername, msgArr[3], selfrename);
break;
}
+ case "turn": {
+ // Reset all turn data
+ for (var user of this.users) user.turn = -1;
+ var queuedUsers = parseInt(msgArr[2]);
+ if (queuedUsers === 0) {
+ this.publicEmitter.emit('turn', {
+ user: null,
+ queue: [],
+ turnTime: null,
+ queueTime: null,
+ } as TurnStatus);
+ return;
+ }
+ var currentTurn = this.users.find(u => u.username === msgArr[3])!;
+ currentTurn.turn = 0;
+ var queue : User[] = [];
+ if (queuedUsers > 1) {
+ for (var i = 1; i < queuedUsers; i++) {
+ var user = this.users.find(u => u.username === msgArr[i+3])!;
+ queue.push(user);
+ user.turn = i;
+ }
+ }
+ this.publicEmitter.emit('turn', {
+ user: currentTurn,
+ queue: queue,
+ turnTime: currentTurn.username === this.username ? parseInt(msgArr[1]) : null,
+ queueTime: queue.some(u => u.username === this.username) ? parseInt(msgArr[msgArr.length - 1]) : null,
+ } as TurnStatus)
+ break;
+ }
}
}
@@ -185,7 +218,7 @@ export default class CollabVMClient {
// Close the connection
close() {
this.connectedToVM = false;
- this.socket.close();
+ if (this.socket.readyState === WebSocket.OPEN) this.socket.close();
}
// Get users
@@ -194,5 +227,16 @@ export default class CollabVMClient {
return this.users.slice();
}
+ // Send a chat message
+ chat(message : string) {
+ this.send("chat", message);
+ }
+
+ // Rename
+ rename(username : string | null = null) {
+ if (username) this.send("rename", username);
+ else this.send("rename");
+ }
+
on = (event : string | number, cb: (...args: any) => void) => this.publicEmitter.on(event, cb);
}
\ No newline at end of file
diff --git a/src/ts/protocol/TurnStatus.ts b/src/ts/protocol/TurnStatus.ts
new file mode 100644
index 0000000..8720cba
--- /dev/null
+++ b/src/ts/protocol/TurnStatus.ts
@@ -0,0 +1,12 @@
+import { User } from "./User.js";
+
+export default interface TurnStatus {
+ // The user currently taking their turn
+ user : User | null;
+ // The users in the turn queue
+ queue : User[];
+ // Amount of time left in the turn. Null unless the user is taking their turn
+ turnTime : number | null;
+ // Amount of time until the user gets their turn. Null unless the user is in the queue
+ queueTime : number | null;
+}
\ No newline at end of file
diff --git a/src/ts/protocol/User.ts b/src/ts/protocol/User.ts
index 2faaabc..c7942a9 100644
--- a/src/ts/protocol/User.ts
+++ b/src/ts/protocol/User.ts
@@ -2,10 +2,13 @@ import { Rank } from "./Permissions.js";
export class User {
username : string;
- rank : Rank
+ rank : Rank;
+ // -1 means not in the turn queue, 0 means the current turn, anything else is the position in the queue
+ turn : number;
constructor(username : string, rank : Rank = Rank.Unregistered) {
this.username = username;
this.rank = rank;
+ this.turn = -1;
}
}
\ No newline at end of file
diff --git a/static/.gitkeep b/static/.gitkeep
new file mode 100644
index 0000000..e69de29