329 lines
12 KiB
JavaScript
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 |