Compare commits

..

2 Commits

Author SHA1 Message Date
peshomir 6259f52b3e Update readme.md 2024-10-14 20:41:11 +03:00
peshomir a59ca4355a Add custom lobbies
Update v0.6.6
2024-10-14 20:29:43 +03:00
9 changed files with 285 additions and 30 deletions

View File

@ -115,7 +115,7 @@ const generateRegularExpression = (/** @type {string} */ code, /** @type {boolea
// if a substitution string for the "word" is specified in the nameMappings, use it // if a substitution string for the "word" is specified in the nameMappings, use it
if (nameMappings && nameMappings.hasOwnProperty(word)) return nameMappings[word]; if (nameMappings && nameMappings.hasOwnProperty(word)) return nameMappings[word];
// if the "word" is a number or is one of these specific words, ingore it // if the "word" is a number or is one of these specific words, ingore it
if (/^\d/.test(word) || ["return", "this", "var", "function", "Math"].includes(word)) return word; if (/^\d/.test(word) || ["return", "this", "var", "function", "new", "Math", "WebSocket"].includes(word)) return word;
// for easy localizer function matching // for easy localizer function matching
else if (word === "___localizer") { else if (word === "___localizer") {
groups[word + (++localizerCount)] = groupNumberCounter++; groups[word + (++localizerCount)] = groupNumberCounter++;

View File

@ -50,14 +50,17 @@ export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, mat
o.ha(sE,2),b.h9<100?xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0):xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0)`); o.ha(sE,2),b.h9<100?xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0):xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0)`);
{ // Add settings button and win count { // Add settings button, custom lobby button and win count
// add settings button // add buttons
replaceRawCode(`,new nQ("☰<br>"+__L(),function(){aD6(3)},aa.ks),new nQ("",function(){at.d5(12)},aa.kg,!1)]`, replaceRawCode(`,new nQ("☰<br>"+__L(),function(){aD6(3)},aa.ks),new nQ("",function(){at.d5(12)},aa.kg,!1)]`,
`,new nQ("☰<br>"+__L(),function(){aD6(3)},aa.ks),new nQ("",function(){at.d5(12)},aa.kg,!1), `,new nQ("☰<br>"+__L(),function(){aD6(3)},aa.ks),new nQ("",function(){at.d5(12)},aa.kg,!1),
new nQ("FX Client settings", function() { __fx.WindowManager.openWindow("settings"); }, "rgba(0, 0, 20, 0.5")]`) new nQ("FX Client settings", function() { __fx.WindowManager.openWindow("settings"); }, "rgba(0, 0, 20, 0.5)"),
// set settings button position new nQ("Join/Create custom lobby", function() { __fx.customLobby.showJoinPrompt(); }, "rgba(20, 9, 77, 0.5)")]`)
// set position
replaceRawCode(`aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h);`, replaceRawCode(`aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h);`,
`aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h); aZ.g5.vO(aD3[5].button, x, a3X + h * 2 + gap * 2, a0S * 2 + gap, h / 3);`); `aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h);
aZ.g5.vO(aD3[5].button, x, a3X + h * 2 + gap * 2, a0S * 2 + gap, h / 3);
aZ.g5.vO(aD3[6].button, x, a3X + h * 2.33 + gap * 3, a0S * 2 + gap, h / 3);`);
// render win count // render win count
replaceRawCode(`if(_y.a4l(),_r.gI(),_m.gI(),aw.gI(),a0.g8()){ctx.imageSmoothingEnabled=!1;var iQ=a0.a4o("territorial.io"),kL=.84*aD4.gA/iQ.width;`, replaceRawCode(`if(_y.a4l(),_r.gI(),_m.gI(),aw.gI(),a0.g8()){ctx.imageSmoothingEnabled=!1;var iQ=a0.a4o("territorial.io"),kL=.84*aD4.gA/iQ.width;`,
`if(_y.a4l(),_r.gI(),_m.gI(),aw.gI(),a0.g8()){ `if(_y.a4l(),_r.gI(),_m.gI(),aw.gI(),a0.g8()){
@ -137,7 +140,7 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
`, ${dict.game}.${dict.gIsTeamGame} && __fx.donationsTracker.displayHistory($2, ${rawPlayerNames}, ${gIsSingleplayer}), $1 && !isEmptySpace $3`); `, ${dict.game}.${dict.gIsTeamGame} && __fx.donationsTracker.displayHistory($2, ${rawPlayerNames}, ${gIsSingleplayer}), $1 && !isEmptySpace $3`);
// Reset donation history and leaderboard filter when a new game is started // Reset donation history and leaderboard filter when a new game is started
replaceOne(new RegExp(`,this\\.${dictionary.playerBalances}.fill\\(0\\),`, "g"), "$& __fx.donationsTracker.reset(), __fx.leaderboardFilter.reset(), "); replaceOne(new RegExp(`,this\\.${dictionary.playerBalances}.fill\\(0\\),`, "g"), "$& __fx.donationsTracker.reset(), __fx.leaderboardFilter.reset(), __fx.customLobby.isActive() && __fx.customLobby.setActive(false), ");
{ // Player list and leaderboard filter tabs { // Player list and leaderboard filter tabs
// Draw player list button // Draw player list button
@ -293,6 +296,37 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
) )
} }
{ // Custom lobbies
replaceRawCode("this.aHm=function(){i___.rX(),aM.a7U(bY.dZ.data[10].value),aM.init()}",
`this.aHm=function(){i___.rX(),aM.a7U(bY.dZ.data[10].value),aM.init()},
__fx.customLobby.setJoinFunction(() => { i___.rX(); aM.a7U(0); aM.init(); })`
)
replaceRawCode(`(socketId-aq.kt.a82)+"/",(socket=new WebSocket(url)`,
`(socketId-aq.kt.a82)+"/",(socket=new WebSocket(__fx.customLobby.isActive() && socketId === 1 ? __fx.customLobby.getSocketURL() : url)`)
replaceRawCode("this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)}",
"this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)},__fx.customLobby.setSendFunction(this.send)")
replaceRawCode("b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):",
"b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):__fx.customLobby.isCustomMessage(a0)||")
// set the custom lobby to inactive when clicking the "Back" button on the connection screen or leaving the lobby
replaceRawCode("this.xZ=function(){Sockets.kt.wf(3260),i___.kt.we()}",
"this.xZ=function(){Sockets.kt.wf(3260),__fx.customLobby.setActive(false),i___.kt.we()}")
replaceRawCode("this.xY=function(){this.wg(),Sockets.kt.wf(3240),aN.setState(0),i___.j(5,5)}",
`this.xY=function(){this.wg(),Sockets.kt.wf(3240),__fx.customLobby.setActive(false),aN.setState(0),i___.j(5,5)},
__fx.customLobby.setLeaveFunction(() => this.xY())`)
/* // if this is needed again, include a check for menu state === 6
replaceRawCode("this.wQ=function(wR,d){if(8===i.pz&&0===wR)if(4211===d)wS(d);",
"this.wQ=function(wR,d){ wR===1&&__fx.customLobby.isActive()&&__fx.customLobby.setActive(false); if(8===i.pz&&0===wR)if(4211===d)wS(d);")*/
// if the server is unreachable
replaceRawCode("0===a7Q?g.wc(3249):", "0===a7Q?g.wc(3249):1===a7Q&&__fx.customLobby.isActive()?(g.wc(3249),__fx.customLobby.setActive(false)):")
// error descriptions
const errors = { 3249: "No servers found", 4705: "Lobby not found" };
replaceRawCode(`i___.j(4,5,new k("⚠️ "+title,w3__,!0))`,
`i___.j(4,5,new k("⚠️ "+title, ${JSON.stringify(errors)}[w3__] || w3__,!0))`)
// map info (for the map selection menu)
replaceRawCode("this.info=new Array(Maps.totalMapCount+1),this.info[0]={name:__L(),",
"this.info=new Array(Maps.totalMapCount+1),__fx.customLobby.setMapInfo(this.info),this.info[0]={name:__L(),")
}
// Invalid hostname detection avoidance // Invalid hostname detection avoidance
replaceRawCode(`,hostnameIsValid=0<=window.location.hostname.toLowerCase().indexOf("territorial.io"),`, replaceRawCode(`,hostnameIsValid=0<=window.location.hostname.toLowerCase().indexOf("territorial.io"),`,
`,hostnameIsValid=true,`) `,hostnameIsValid=true,`)

View File

@ -25,17 +25,18 @@ FX Client is the first Territorial.io client, offering a better User Interface a
4. Displays your troop density and maximum troops 4. Displays your troop density and maximum troops
5. Displays the density of players and bots 5. Displays the density of players and bots
6. Adds a "Clan" tab on the leaderboard, allowing you to easily see your clanmates 6. Adds a "Clan" tab on the leaderboard, allowing you to easily see your clanmates
7. Hovering tooltip: makes the territory map information (normally visible on right click) be visible constantly (on hover) 7. Adds custom lobbies
8. Adds a player list 8. Hovering tooltip: makes the territory map information (normally visible on right click) be visible constantly (on hover)
9. Adds the ability to view the history of who donated to a player during a team game by clicking on their name in the leaderboard or the player list 9. Adds a player list
10. Adds a win counter 10. Adds the ability to view the history of who donated to a player during a team game by clicking on their name in the leaderboard or the player list
11. Can be installed as a PWA (progressive web app) ensuring maximum enjoyment on consoles, phones and even desktop devices 11. Adds a win counter
12. Can be installed as a PWA (progressive web app) ensuring maximum enjoyment on consoles, phones and even desktop devices
#### The client has a settings menu, from which you can: #### The client has a settings menu, from which you can:
12. Make fullscreen mode trigger automatically 13. Make fullscreen mode trigger automatically
13. Set a custom main menu background 14. Set a custom main menu background
14. Create custom attack percentage keybinds 15. Create custom attack percentage keybinds
## Building Locally ## Building Locally

181
src/customLobby.js 100644
View File

@ -0,0 +1,181 @@
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();
/*const gameInfo = {
botCount: 512
}*/
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 playerList = document.createElement("div");
playerListContainer.append(playerCount, playerList);
// todo: convert the options into something similar to the automatic settings generator
const optionsContainer = document.createElement("div");
const gameModeSelectMenu = document.createElement("select");
setSelectMenuOptions([
{ 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" }
], gameModeSelectMenu);
gameModeSelectMenu.value = "7";
const mapSelectMenu = document.createElement("select");
function setMapInfo(maps) {
setTimeout(() => setSelectMenuOptions(maps.map((info, index) => ({ value: index.toString(), label: info.name })), mapSelectMenu), 0);
}
gameModeSelectMenu.addEventListener("change", e => sendMessage("options", ["mode", parseInt(e.target.value)]));
mapSelectMenu.addEventListener("change", e => sendMessage("options", ["map", parseInt(e.target.value)]));
/*const botCountInput = document.createElement("input");
botCountInput.setAttribute("type", "number");
botCountInput.setAttribute("min", "0");
botCountInput.setAttribute("max", "512");
botCountInput.value = "512";*/
optionsContainer.append(
"Mode: ", gameModeSelectMenu, document.createElement("br"),
"Map: ", mapSelectMenu, document.createElement("br"),
//"Bot count: "
);
main.append(playerListContainer, optionsContainer);
const footer = document.createElement("footer");
footer.style.marginTop = "10px";
const startButton = document.createElement("button");
const leaveButton = document.createElement("button");
startButton.textContent = "Start game";
leaveButton.textContent = "Leave lobby";
startButton.addEventListener("click", startGame);
leaveButton.addEventListener("click", () => leaveLobby());
footer.append(leaveButton, startButton);
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() {
const code = prompt("Enter a lobby code or leave blank to create a new lobby");
if (code === null) return;
currentCode = code;
//WindowManager.openWindow("customLobby");
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);
}
/** @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;
gameModeSelectMenu.value = data.options.mode.toString();
mapSelectMenu.value = data.options.map.toString();
displayPlayers(data.players);
} else if (type === "addPlayer") addPlayer(data);
else if (type === "removePlayer") {
const index = data;
playerElements[index].remove();
playerElements.splice(index, 1);
updatePlayerCount();
} else if (type === "options") {
console.log(data);
const [option, value] = data;
if (option === "mode") gameModeSelectMenu.value = value.toString();
else if (option === "map") mapSelectMenu.value = value.toString();
}
return true;
}
/** @type {HTMLDivElement[]} */
let playerElements = [];
/** @param {{ name: string }} player */
function addPlayer(player) {
const div = document.createElement("div");
div.className = "lobby-player";
div.textContent = player.name;
playerList.append(div);
playerElements.push(div);
updatePlayerCount();
}
/** @param {{ name: string }[]} players */
function displayPlayers(players) {
playerElements = [];
playerList.innerHTML = "";
players.forEach(addPlayer);
updatePlayerCount();
}
function updatePlayerCount() {
playerCount.textContent = `${playerElements.length} Player${playerElements.length === 1 ? "" : "s"}`
}
function getSocketURL() {
return socketURL + (currentCode === "" ? "create" : "join?" + currentCode)
}
function startGame() {
WindowManager.closeWindow("customLobby");
sendMessage("startGame" /*{ mode: gameModeSelectMenu.value, map: mapSelectMenu.value }*/);
}
function setJoinFunction(f) { joinLobby = f; }
function setLeaveFunction(f) { leaveLobby = f; }
function setSendFunction(f) { sendRaw = f; }
function setActive(active) {
isActive = active;
if (active === false) WindowManager.closeWindow("customLobby");
}
const gameInterface = { showJoinPrompt, isCustomMessage, getSocketURL, setJoinFunction, setLeaveFunction, setSendFunction, setMapInfo, isActive: () => isActive, setActive }
const customLobby = gameInterface
export default customLobby

View File

@ -1,7 +1,7 @@
const playerDataProperties = ["playerTerritories", "playerBalances", "rawPlayerNames"]; const playerDataProperties = ["playerTerritories", "playerBalances", "rawPlayerNames"];
const gameObjectProperties = ["playerId", "gIsTeamGame", "gHumans", "gLobbyMaxJoin", "gameState", "gIsSingleplayer"]; const gameObjectProperties = ["playerId", "gIsTeamGame", "gHumans", "gLobbyMaxJoin", "gameState", "gIsSingleplayer"];
export const getVar = varName => { export const getVar = varName => {
if (playerDataProperties.includes(varName)) return window[dictionary.playerData][dictionary[varName]]; if (playerDataProperties.includes(varName)) return window[dictionary.playerData]?.[dictionary[varName]];
if (gameObjectProperties.includes(varName)) return window[dictionary.game][dictionary[varName]]; if (gameObjectProperties.includes(varName)) return window[dictionary.game]?.[dictionary[varName]];
return window[dictionary[varName]] return window[dictionary[varName]]
}; };

View File

@ -1,5 +1,5 @@
const fx_version = '0.6.5.6'; // FX Client Version const fx_version = '0.6.6'; // FX Client Version
const fx_update = 'Oct 3'; // FX Client Last Updated const fx_update = 'Oct 14'; // FX Client Last Updated
import settingsManager from './settings.js'; import settingsManager from './settings.js';
import { clanFilter, leaderboardFilter } from "./clanFilters.js"; import { clanFilter, leaderboardFilter } from "./clanFilters.js";
@ -10,6 +10,7 @@ import playerList from "./playerList.js";
import gameScriptUtils from "./gameScriptUtils.js"; import gameScriptUtils from "./gameScriptUtils.js";
import hoveringTooltip from "./hoveringTooltip.js"; import hoveringTooltip from "./hoveringTooltip.js";
import { keybindFunctions, keybindHandler } from "./keybinds.js"; import { keybindFunctions, keybindHandler } from "./keybinds.js";
import customLobby from './customLobby.js';
window.__fx = window.__fx || {}; window.__fx = window.__fx || {};
const __fx = window.__fx; const __fx = window.__fx;
@ -26,5 +27,6 @@ __fx.playerList = playerList;
__fx.hoveringTooltip = hoveringTooltip; __fx.hoveringTooltip = hoveringTooltip;
__fx.clanFilter = clanFilter; __fx.clanFilter = clanFilter;
__fx.wins = winCounter; __fx.wins = winCounter;
__fx.customLobby = customLobby;
console.log('Successfully loaded FX Client'); console.log('Successfully loaded FX Client');

View File

@ -1,4 +1,14 @@
var windows = {}; var windows = {};
const container = document.getElementById("windowContainer");
function create(info) {
const window = document.createElement("div");
info.element = window;
window.className = "window" + (info.classes !== undefined ? " " + info.classes : " scrollable selectable");
window.style.display = "none";
container.appendChild(window);
add(info);
return window;
}
function add(newWindow) { function add(newWindow) {
windows[newWindow.name] = newWindow; windows[newWindow.name] = newWindow;
windows[newWindow.name].isOpen = false; windows[newWindow.name].isOpen = false;
@ -17,11 +27,11 @@ function closeWindow(windowName) {
}; };
function closeAll() { function closeAll() {
Object.values(windows).forEach(function (windowObj) { Object.values(windows).forEach(function (windowObj) {
closeWindow(windowObj.name); if (windowObj.closable !== false) closeWindow(windowObj.name);
}); });
}; };
document.getElementById("canvasA").addEventListener("mousedown", closeAll); document.getElementById("canvasA").addEventListener("mousedown", closeAll);
document.getElementById("canvasA").addEventListener("touchstart", closeAll, { passive: true }); document.getElementById("canvasA").addEventListener("touchstart", closeAll, { passive: true });
document.addEventListener("keydown", event => { if (event.key === "Escape") closeAll(); }); document.addEventListener("keydown", event => { if (event.key === "Escape") closeAll(); });
export default { add, openWindow, closeWindow, closeAll } export default { create, add, openWindow, closeWindow, closeAll }

View File

@ -59,7 +59,7 @@
<body onload="aiCommand746(0);"> <body onload="aiCommand746(0);">
<canvas id="canvasA" width="128" height="128"></canvas> <canvas id="canvasA" width="128" height="128"></canvas>
<span><div class="window flex settings" style="display:none"> <span id="windowContainer"><div class="window flex-column settings" style="display:none">
<h1>Settings</h1> <h1>Settings</h1>
<div class="scrollable"></div> <div class="scrollable"></div>
<hr> <hr>

View File

@ -34,11 +34,31 @@
z-index : 10; z-index : 10;
} }
.window.flex { .flex {
display: flex;
}
.flex-column {
display : flex; display : flex;
flex-direction: column; flex-direction: column;
} }
.customlobby-main {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
gap: 10px;
}
.lobby-player {
margin: 5px;
width: 15rem;
}
.text-align-center {
text-align: center;
}
hr { hr {
width: 100%; width: 100%;
} }
@ -54,6 +74,13 @@ hr {
transition : 0.2s; transition : 0.2s;
border : 1px solid #fff; border : 1px solid #fff;
border-radius : 5px; border-radius : 5px;
margin : 5px;
}
.window.settings button,
.window.settings input,
.window.settings select {
margin: 0px;
} }
h1 { h1 {
@ -96,16 +123,16 @@ td {
#playerlist_content.clickable td:hover { background-color: #00ff0040; } #playerlist_content.clickable td:hover { background-color: #00ff0040; }
tr.new { tr.new {
animation: flashAnimation 0.4s ease-out; animation: flashAnimation 0.4s ease-out;
} }
@keyframes flashAnimation { @keyframes flashAnimation {
0% { 0% {
background-color: #ffffffaa; background-color: #ffffffaa;
} }
100% { 100% {
background-color: transparent; background-color: transparent;
} }
} }
table { table {