FXclient-mirror/src/customLobby.js

329 lines
12 KiB
JavaScript

import WindowManager from "./windowManager.js";
//const socketURL = "ws://localhost:8080/";
const socketURL = "wss://fx.peshomir.workers.dev/";
const customMessageMarker = 120;
let isActive = false;
let currentCode = "";
let joinLobby = () => { };
let leaveLobby = () => { };
let sendRaw = (socketId, data) => { };
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
WindowManager.add({
name: "lobbyJoinMenu",
element: document.getElementById("customLobbyJoinMenu")
})
const windowElement = WindowManager.create({
name: "customLobby",
classes: "scrollable selectable flex-column text-align-center",
closable: false
});
const header = document.createElement("h2");
header.textContent = "Custom Lobby";
const main = document.createElement("div");
main.className = "customlobby-main";
const playerListContainer = document.createElement("div");
const playerCount = document.createElement("p");
playerCount.textContent = "0 Players";
const playerListDiv = document.createElement("div");
playerListContainer.append(playerCount, playerListDiv);
const optionsContainer = document.createElement("div");
optionsContainer.className = "text-align-left";
const optionsStructure = {
mode: {
label: "Mode:", type: "selectMenu", options: [
{ value: 0, label: "2 Teams" },
{ value: 1, label: "3 Teams" },
{ value: 2, label: "4 Teams" },
{ value: 3, label: "5 Teams" },
{ value: 4, label: "6 Teams" },
{ value: 5, label: "7 Teams" },
{ value: 6, label: "8 Teams" },
{ value: 7, label: "Battle Royale" },
{ value: 10, label: "No Fullsend Battle Royale" },
{ value: 9, label: "Zombie mode" }
]
},
map: { label: "Map:", type: "selectMenu" },
difficulty: { label: "Difficulty:", type: "selectMenu", options: [
{ value: 0, label: "Very Easy (Default)" },
{ value: 1, label: "Easy (1v1)" },
{ value: 2, label: "Normal" },
{ value: 3, label: "Hard" },
{ value: 4, label: "Very Hard" },
{ value: 5, label: "Impossible" }
]},
spawnSelection: { label: "Spawn selection", type: "checkbox" },
botCount: { label: "Bot & player count:", type: "numberInput", attributes: { min: "1", max: "512" } }
}
const optionsElements = {};
const optionsValues = {};
function updateOption(option, value) {
if (optionsStructure[option].type === "checkbox")
optionsElements[option].checked = (value === 0 ? false : true);
else optionsElements[option].value = value.toString();
optionsValues[option] = value;
}
function inputUpdateHandler(key, e) {
sendMessage("options", [key, parseInt(e.target.value)])
}
function checkboxUpdateHandler(key, e) {
sendMessage("options", [key, e.target.checked ? 1 : 0])
}
Object.entries(optionsStructure).forEach(([key, item]) => {
const label = document.createElement("label");
if (item.tooltip) label.title = item.tooltip;
const isValueInput = item.type.endsWith("Input");
const element = document.createElement(
isValueInput || item.type === "checkbox" ? "input"
: item.type === "selectMenu" ? "select"
: "button"
);
optionsElements[key] = element;
if (item.type === "textInput") element.type = "text";
if (item.type === "numberInput") element.type = "number";
if (item.placeholder) element.placeholder = item.placeholder;
if (isValueInput || item.type === "selectMenu")
element.addEventListener("change", inputUpdateHandler.bind(undefined, key))
if (item.text) element.innerText = item.text;
if (item.action) element.addEventListener("click", item.action);
if (item.label) label.append(item.label + " ");
if (item.note) {
const note = document.createElement("small");
note.innerText = item.note;
label.append(document.createElement("br"), note);
}
if (item.options) setSelectMenuOptions(item.options, element);
if (item.attributes) Object.entries(item.attributes).forEach(
([name, value]) => element.setAttribute(name, value)
);
label.append(element);
if (item.type === "checkbox") {
element.type = "checkbox";
const checkmark = document.createElement("span");
checkmark.className = "checkmark";
label.className = "checkbox";
label.append(checkmark);
//checkboxFields[item.for] = element;
element.addEventListener("change", checkboxUpdateHandler.bind(undefined, key))
} else label.append(document.createElement("br"));
optionsContainer.append(label/*, document.createElement("br")*/);
});
function setMapInfo(maps) {
setTimeout(() => setSelectMenuOptions(maps.map((info, index) => ({ value: index.toString(), label: info.name })), optionsElements["map"]), 0);
}
main.append(playerListContainer, optionsContainer);
const footer = document.createElement("footer");
footer.style.marginTop = "10px";
function createButton(text, action) {
const button = document.createElement("button");
button.textContent = text;
button.addEventListener("click", action);
return button;
}
const startButton = createButton("Start game", startGame);
const leaveButton = createButton("Leave lobby", () => leaveLobby());
const copyLinkButton = createButton("Copy link", () => {
navigator.clipboard.writeText(`${window.location.href}#lobby=${currentCode}`);
copyLinkButton.textContent = "Copied!";
setTimeout(() => copyLinkButton.textContent = "Copy link", 1000);
});
footer.append(startButton, leaveButton, copyLinkButton);
windowElement.append(header, main, footer);
/** @param {HTMLSelectElement} element */
function setSelectMenuOptions(options, element) {
options.forEach(data => {
const option = document.createElement("option");
option.setAttribute("value", data.value);
option.textContent = data.label;
element.append(option);
})
}
function showJoinPrompt() {
WindowManager.openWindow("lobbyJoinMenu");
}
document.getElementById("lobbyCode").addEventListener("input", ({ target: input }) => {
if (input.value.length !== 5) return;
currentCode = input.value.toLowerCase();
input.value = "";
WindowManager.closeWindow("lobbyJoinMenu");
isActive = true;
joinLobby();
});
document.getElementById("createLobbyButton").addEventListener("click", () => {
currentCode = "";
WindowManager.closeWindow("lobbyJoinMenu");
isActive = true;
joinLobby();
});
function sendMessage(type, data) {
const message = data !== undefined ? { t: type, d: data } : { t: type }
const originalArray = textEncoder.encode(JSON.stringify(message));
const buffer = new ArrayBuffer(originalArray.length + 1);
const view = new DataView(buffer);
// Set the first byte to the custom message marker
view.setUint8(0, customMessageMarker);
// Copy the original array starting from the second byte
const uint8ArrayView = new Uint8Array(buffer, 1);
uint8ArrayView.set(originalArray);
sendRaw(1, buffer);
}
let playerIsHost = false;
/** @param {Uint8Array} raw */
function isCustomMessage(raw) {
if (raw[0] !== customMessageMarker) return false;
if (raw.length === 1) return true; // ping
const subArray = new Uint8Array(raw.buffer, 1);
const message = JSON.parse(textDecoder.decode(subArray));
const { t: type, d: data } = message;
if (type === "lobby") {
WindowManager.openWindow("customLobby");
header.textContent = "Custom Lobby " + data.code;
currentCode = data.code;
playerIsHost = data.isHost;
startButton.disabled = !playerIsHost;
if (playerIsHost) optionsContainer.classList.remove("disabled");
else optionsContainer.classList.add("disabled");
Object.entries(data.options).forEach(([option, value]) => updateOption(option, value));
displayPlayers(data.players, data.id);
} else if (type === "addPlayer") {
addPlayer({ name: data.name, inGame: false, isHost: false });
updatePlayerCount();
} else if (type === "removePlayer") {
const index = data;
playerList[index].element.remove();
playerList.splice(index, 1);
updatePlayerCount();
} else if (type === "inLobby") {
const index = data;
playerList[index].inGame = false;
playerList[index].inGameBadge.className = "d-none";
} else if (type === "options") {
const [option, value] = data;
updateOption(option, value);
} else if (type === "setHost") {
const index = data;
playerList[index].isHost = true;
playerList[index].hostBadge.className = "";
} else if (type === "host") {
playerIsHost = true;
startButton.disabled = false;
optionsContainer.classList.remove("disabled");
playerList.forEach(p => { if (!p.isHost) p.kickButton.className = "" });
} else if (type === "serverMessage") alert(data);
return true;
}
/** @typedef {{ element: HTMLDivElement, hostBadge: HTMLSpanElement, inGameBadge: HTMLSpanElement, kickButton: HTMLButtonElement, isHost: boolean, inGame: boolean }} PlayerListEntry */
/** @type {PlayerListEntry[]} */
let playerList = [];
/** @type {PlayerListEntry} */
let thisPlayer;
function createBadge(text, visible) {
const badge = document.createElement("span");
badge.textContent = text;
badge.className = visible ? "" : "d-none";
return badge;
}
/** @typedef {{ name: string, isHost: boolean, inGame: boolean }} PlayerInfo */
/** @param {PlayerInfo} player */
function addPlayer(player) {
const div = document.createElement("div");
div.className = "lobby-player";
div.textContent = player.name;
const kickButton = document.createElement("button");
kickButton.textContent = "Kick";
kickButton.className = playerIsHost && !player.isHost ? "" : "d-none";
kickButton.addEventListener("click", kickButtonHandler);
const hostBadge = createBadge("Host", player.isHost);
const inGameBadge = createBadge("In Game", player.inGame);
div.append(hostBadge, inGameBadge, kickButton);
playerListDiv.append(div);
playerList.push({ element: div, hostBadge, inGameBadge, kickButton, isHost: player.isHost, inGame: player.inGame });
}
function kickButtonHandler(event) {
const button = event.target;
for (let index = 0; index < playerList.length; index++) {
if (playerList[index].kickButton === button) {
sendMessage("kick", index);
break;
}
}
}
/** @param {PlayerInfo[]} players */
function displayPlayers(players, thisPlayerId) {
playerList = [];
playerListDiv.innerHTML = "";
players.forEach(addPlayer);
thisPlayer = playerList[thisPlayerId];
updatePlayerCount();
}
function updatePlayerCount() {
playerCount.textContent = `${playerList.length} Player${playerList.length === 1 ? "" : "s"}`
}
function getSocketURL() {
return socketURL + (currentCode === "" ? "create" : "join?" + currentCode)
}
function getPlayerId() {
let id = 0;
for (let i = 0; i < playerList.length; i++) {
const player = playerList[i];
if (player === thisPlayer) return id;
if (player.inGame === false) id++;
}
}
function startGame() {
WindowManager.closeWindow("customLobby");
sendMessage("startGame");
}
function rejoinLobby() {
joinLobby();
}
function checkForLobbyLink(isHashChangeEvent) {
const hash = window.location.hash;
if (hash.startsWith("#lobby=")) {
// in case the player is already in a lobby
if (isHashChangeEvent) leaveLobby();
currentCode = hash.slice(7);
isActive = true;
joinLobby();
}
}
window.addEventListener("hashchange", () => checkForLobbyLink(true));
function setJoinFunction(f) {
joinLobby = f;
setTimeout(checkForLobbyLink, 0);
}
function setLeaveFunction(f) { leaveLobby = f; }
function setSendFunction(f) { sendRaw = f; }
function setActive(active) {
isActive = active;
if (active === false) WindowManager.closeWindow("customLobby");
}
function hideWindow() {
WindowManager.closeWindow("customLobby");
}
const gameInterface = { gameInfo: optionsValues, showJoinPrompt, isCustomMessage, getSocketURL, getPlayerId, setJoinFunction, setLeaveFunction, setSendFunction, setMapInfo, rejoinLobby, hideWindow, isActive: () => isActive, setActive }
const customLobby = gameInterface
export default customLobby