// ==UserScript==
// @name FaustCore
// @version 2025-02-21
// @description Adds support for Faust mods
// @author Faust
// @match https://esonline.su/
// @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @grant none
// @namespace https://gf.qytechs.cn/users/1437749
// ==/UserScript==
(function() {
"use strict";
var MessageReason;
(function (MessageReason) {
MessageReason.user_join = "userJoin";
MessageReason.user_leave = "userLeave";
MessageReason.node_data = "nodeData";
MessageReason.time_update = "serverTimecode";
MessageReason.client_move = "clientMove";
})(MessageReason || (MessageReason = {}));
function getScreenResolution() {
const resolutions = [
[1920, 1080],
[1792, 1008],
[1664, 936],
[1536, 864],
[1408, 792],
[1280, 720],
[1152, 648],
[1024, 576],
[896, 504],
[768, 432],
[640, 360],
[512, 288],
[384, 216],
];
const screen_width = window.innerWidth;
const screen_height = window.innerHeight;
let selected_resolution;
for (let index in resolutions) {
let resolution = resolutions[index];
if (resolution[0] <= screen_width && resolution[1] <= screen_height) {
selected_resolution = resolution;
break;
}
}
if (!selected_resolution) {
console.error("screen too small " + screen_width + "x" + screen_height);
selected_resolution = resolutions[resolutions.length - 1];
}
return selected_resolution;
}
function display_notification(text) {
const elements = document.querySelectorAll("#notify");
if (elements.length > 0 && elements[0].parentElement) {
elements[0].parentElement.removeChild(elements[0]);
}
const text_bubble = document.createElement("div");
text_bubble.id = "notify";
text_bubble.innerHTML = text;
text_bubble.style.animationName = "hide";
text_bubble.style.animationDelay = "2s";
text_bubble.style.animationDuration = "0.5s";
text_bubble.style.animationFillMode = "forwards";
text_bubble.style.zIndex = "9999";
document.body.appendChild(text_bubble);
text_bubble.addEventListener("animationend", () => {
text_bubble.remove();
});
}
class AbstractMenuItem {
constructor() {
this.is_added = false;
this.is_hidden = false;
this.main_div = document.createElement("div");
}
addElement() {
if (this.is_added) {
return;
}
this.main_div = this.composeMainDiv();
document.body.appendChild(this.main_div);
this.is_added = true;
}
removeElement() {
if (!this.is_added) {
return;
}
this.main_div.remove();
this.is_added = false;
}
hideElement() {
if (!this.is_added) {
return;
}
this.main_div.style.display = "none";
this.is_hidden = true;
}
unhideElement() {
if (!this.is_added) {
return;
}
this.main_div.style.display = "block";
this.is_hidden = false;
}
isShown() {
return this.is_added;
}
isHidden() {
return this.is_hidden;
}
}
class MenuButtonsFactory {
static buildButton(text, on_click) {
const button = document.createElement("button");
button.style.borderRadius = "10px";
button.style.border = "2px solid rgba(3, 55, 170, 0.8)";
button.style.backgroundColor = "rgba(3, 55, 170, 0.8)";
button.style.color = "white";
button.style.padding = "5px 10px";
button.style.fontSize = "14px";
button.innerText = text;
button.addEventListener("mouseenter", () => {
button.style.backgroundColor = "white";
button.style.color = "rgba(3, 55, 170, 0.8)";
});
button.addEventListener("mouseleave", () => {
button.style.backgroundColor = "rgba(3, 55, 170, 0.8)";
button.style.color = "white";
});
button.addEventListener("mousedown", on_click);
return button;
}
}
class TweakMenu extends AbstractMenuItem {
constructor() {
super();
}
getScrollableArea(screen_height) {
const scrollableDiv = document.createElement("div");
scrollableDiv.style.height = (screen_height / 3).toString() + "px";
scrollableDiv.style.overflow = "auto";
scrollableDiv.style.border = "1px solid #ccc";
scrollableDiv.style.padding = "10px";
scrollableDiv.style.backgroundColor = "rgba(0, 0, 0, 0.1)";
scrollableDiv.style.borderRadius = "10px";
scrollableDiv.style.marginBottom = "20px";
return scrollableDiv;
}
getTweakNameColor(tweak) {
if (tweak.isEnabled()) {
return "green";
}
return "white";
}
buildDivUponTweak() {
const result = [];
for (let iter = 0; iter < loaded_tweaks.length; iter++) {
const entry = document.createElement("div");
entry.style.display = "flex";
entry.style.flexDirection = "row";
entry.style.alignItems = "center";
entry.style.justifyContent = "flex-start";
entry.style.borderRadius = "6px";
entry.style.border = "2px solid rgba(230, 224, 203, 0.6)";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = loaded_tweaks[iter].getTweakName();
checkbox.style.marginLeft = "10px";
checkbox.style.marginRight = "10px";
checkbox.checked = loaded_tweaks[iter].isEnabled();
checkbox.addEventListener("change", (event) => {
const target = event.target;
target.checked ? loaded_tweaks[iter].enable() : loaded_tweaks[iter].disable();
});
const label = document.createElement("label");
label.htmlFor = checkbox.id;
label.innerText = checkbox.id;
label.style.fontSize = "21px";
label.style.color = this.getTweakNameColor(loaded_tweaks[iter]);
entry.appendChild(checkbox);
entry.appendChild(label);
result.push(entry);
}
return result;
}
composeMainDiv() {
const result = document.createElement("div");
// resizing
const resolution = getScreenResolution();
result.style.position = "fixed";
result.style.width = (resolution[0] / 2.5).toString() + "px";
result.style.height = "fit-content";
// Centering block
result.style.top = "50%";
result.style.left = "50%";
result.style.transform = "translate(-50%, -50%)";
// Cosmetics
result.style.padding = "20px";
result.style.backgroundColor = "rgba(0, 0, 0, 0.75)"; // transparent black
result.style.borderRadius = "10px";
result.style.textAlign = "center";
result.style.backdropFilter = "blur(5px)";
result.style.zIndex = "9999";
const window_name = document.createElement("h2");
window_name.style.color = "white";
window_name.style.marginBottom = "20px";
window_name.innerText = "Меню твиков";
// TODO: add macros for resolution results (or even better, return a struct)
const scroll_area = this.getScrollableArea(resolution[1]);
const div_tweaks = this.buildDivUponTweak();
for (let iter = 0; iter < div_tweaks.length; iter++) {
scroll_area.appendChild(div_tweaks[iter]);
}
result.appendChild(window_name);
result.appendChild(scroll_area);
result.appendChild(MenuButtonsFactory.buildButton("Close", () => this.removeElement()));
return result;
}
}
class GratitudeMenu extends AbstractMenuItem {
constructor() {
super();
}
getScrollableArea(screen_height) {
const scrollableDiv = document.createElement("div");
scrollableDiv.style.height = (screen_height / 4).toString() + "px";
scrollableDiv.style.overflow = "auto";
scrollableDiv.style.border = "1px solid #ccc";
scrollableDiv.style.padding = "10px";
scrollableDiv.style.backgroundColor = "rgba(0, 0, 0, 0.1)";
scrollableDiv.style.borderRadius = "10px";
scrollableDiv.style.marginBottom = "20px";
return scrollableDiv;
}
composeMainDiv() {
const result = document.createElement("div");
// resizing
const resolution = getScreenResolution();
result.style.position = "fixed";
result.style.width = (resolution[0] / 3).toString() + "px";
result.style.height = "fit-content";
// Centering block
result.style.top = "50%";
result.style.left = "50%";
result.style.transform = "translate(-50%, -50%)";
// Cosmetics
result.style.padding = "20px";
result.style.backgroundColor = "rgba(0, 0, 0, 0.75)"; // transparent black
result.style.borderRadius = "10px";
result.style.textAlign = "center";
result.style.backdropFilter = "blur(5px)";
result.style.zIndex = "9999";
const window_name = document.createElement("h2");
window_name.style.color = "white";
window_name.style.marginBottom = "20px";
window_name.innerText = "Благодарности";
const scroll_area = this.getScrollableArea(resolution[1]);
const gratitude_block = document.createElement("div");
gratitude_block.style.color = "white";
gratitude_block.style.fontSize = "16px";
gratitude_block.innerText =
"Спасибо тебе, игрок, за то что даёшь проекту шанс на жизнь.\nТак же спасибо ребятами ждущим альфа-тест сюжетного твика. Ваша поддержка заставляла проект жить.\n\n Спасибо, TRIOLA.\nСпасибо, makter.";
result.appendChild(window_name);
result.appendChild(scroll_area);
scroll_area.appendChild(gratitude_block);
result.appendChild(MenuButtonsFactory.buildButton("Close", () => this.removeElement()));
return result;
}
}
/// <reference path="./menu_interface.ts" />
/// <reference path="../menu_button_factory.ts" />
/// <reference path="./tweaks_menu.ts" />
/// <reference path="./gratitude_menu.ts" />
class FaustCoreMenu extends AbstractMenuItem {
constructor() {
super();
this.tweak_menu = new TweakMenu();
this.gratitude_menu = new GratitudeMenu();
}
composeMainDiv() {
const result = document.createElement("div");
// positioning
result.style.position = "absolute";
result.style.left = "50%";
result.style.top = "5%";
result.style.transform = "translate(-50%, -50%)";
// reshaping
result.style.width = "fit-content";
result.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
result.style.color = "white";
result.style.zIndex = "9990";
result.style.borderRadius = "10px";
result.style.textAlign = "center";
result.style.backdropFilter = "blur(5px)";
// element positioning
result.style.display = "flex";
result.style.flexDirection = "row";
result.style.alignItems = "center";
result.style.justifyContent = "flex-start";
result.style.gap = "20px";
result.style.padding = "10px";
const window_name = document.createElement("h2");
window_name.style.fontWeight = "bold";
window_name.style.color = "white";
window_name.innerText = "Faust Core";
result.appendChild(window_name);
result.appendChild(MenuButtonsFactory.buildButton("Твики", () => this.tweak_menu.addElement()));
result.appendChild(MenuButtonsFactory.buildButton("?", () => this.gratitude_menu.addElement()));
return result;
}
}
/// <reference path="./menu_windows/faust_core_menu.ts" />
class Application {
constructor() {
this.fcore_menu = new FaustCoreMenu();
this.fcore_menu.addElement();
}
}
const app = new Application();
const OriginalWebSocket = window.WebSocket; // backup original websocket
var WebsocketListeners;
(function (WebsocketListeners) {
let receive_listeners = new Map();
let send_listeners = new Map();
function addReceiveListener(listener) {
if (receive_listeners.has(listener.getTweakName())) {
return false;
}
receive_listeners.set(listener.getTweakName(), listener);
return true;
}
WebsocketListeners.addReceiveListener = addReceiveListener;
function removeReceiveListener(tweak_name) {
if (typeof tweak_name !== "string") {
tweak_name = tweak_name.getTweakName();
}
receive_listeners.delete(tweak_name);
}
WebsocketListeners.removeReceiveListener = removeReceiveListener;
function addSendListener(listener) {
if (send_listeners.has(listener.getTweakName())) {
return false;
}
send_listeners.set(listener.getTweakName(), listener);
return true;
}
WebsocketListeners.addSendListener = addSendListener;
function removeSendListener(tweak_name) {
if (typeof tweak_name !== "string") {
tweak_name = tweak_name.getTweakName();
}
send_listeners.delete(tweak_name);
}
WebsocketListeners.removeSendListener = removeSendListener;
function triggerReceiveListener(data) {
for (let [_, value] of receive_listeners) {
value.processReceivedWebsocketMessage(data);
}
}
WebsocketListeners.triggerReceiveListener = triggerReceiveListener;
function triggerSendListener(data) {
for (let [_, value] of send_listeners) {
value.processSendedWebsocketMessage(data);
}
}
WebsocketListeners.triggerSendListener = triggerSendListener;
})(WebsocketListeners || (WebsocketListeners = {}));
class PatchedWebSocket extends OriginalWebSocket {
constructor(url, protocols) {
super(url, protocols);
// redefining send method
const originalSend = this.send;
this.send = (data) => {
originalSend.call(this, data);
WebsocketListeners.triggerSendListener(data);
};
// redefining accept method
this.addEventListener("message", (event) => {
WebsocketListeners.triggerReceiveListener(event.data);
});
}
}
// Redefining global websocket with Faust patch
window.WebSocket = PatchedWebSocket;
Object.defineProperties(window.WebSocket, {
CONNECTING: { value: OriginalWebSocket.CONNECTING },
OPEN: { value: OriginalWebSocket.OPEN },
CLOSING: { value: OriginalWebSocket.CLOSING },
CLOSED: { value: OriginalWebSocket.CLOSED },
});
const loaded_tweaks = [];
class Tweaks {
constructor() {
loaded_tweaks.push(this);
}
getTweakName() {
return this.name;
}
isServerSide() {
return this.sever_side;
}
isEnabled() {
return this.is_enabled;
}
}
/// <reference path="./tweak_interface.ts" />
class PlayerLister extends Tweaks {
constructor() {
super();
this.name = "Отображать меню с игроками на локации";
this.sever_side = false;
this.is_enabled = false;
// Init main div
this.main_div = this.initPlayerListBlock();
this.main_div.style.display = "none"; // Hiding window
document.body.appendChild(this.main_div);
// sub div will remain empty
this.player_list_div = document.createElement("div");
this.player_list_div.style.padding = "20px";
this.main_div.appendChild(this.player_list_div);
this.is_dragging = false;
this.offset_x = 0;
this.offset_y = 0;
this.players_on_location = new Map();
}
enable() {
this.is_enabled = true;
this.main_div.style.display = "block";
WebsocketListeners.addReceiveListener(this);
display_notification("Для обновления списка перезайдите на локацию.");
}
disable() {
this.is_enabled = false;
this.main_div.style.display = "none";
WebsocketListeners.removeReceiveListener(this);
// Clearing info
this.players_on_location.clear();
this.updatePlayerList();
}
/**
* @brief Parses user json entry from websocket
*
* @param user json info
* @return PlayerInfo: parsed data
*/
parseSocketUserData(user) {
const result = {
id: user.id,
name: user.name,
color: user.color,
};
return result;
}
processReceivedWebsocketMessage(websocket_event_data) {
try {
const parsedData = JSON.parse(websocket_event_data);
switch (parsedData.reason) {
case MessageReason.user_join:
const user = this.parseSocketUserData(parsedData.user);
this.players_on_location.set(user.id, user);
break;
case MessageReason.node_data:
this.players_on_location.clear();
const users = parsedData.users;
if (!Array.isArray(users)) {
return;
}
users.forEach((user) => {
user = this.parseSocketUserData(user);
this.players_on_location.set(user.id, user);
});
break;
case MessageReason.user_leave:
this.players_on_location.delete(parsedData.id);
break;
default:
return;
}
this.updatePlayerList();
}
catch (error) {
console.error(error);
}
}
processSendedWebsocketMessage(websocket_event_data) { }
updatePlayerList() {
this.player_list_div.innerHTML = ""; // Clearing list
for (const [key, value] of this.players_on_location) {
const player_entry = document.createElement("p");
player_entry.style.color = "#" + value.color;
player_entry.innerText = key.toString() + " " + value.name;
// player_entry.style.marginLeft = "10px";
this.player_list_div.appendChild(player_entry);
}
}
initPlayerListBlock() {
const movableDiv = document.createElement("div");
movableDiv.style.position = "fixed";
movableDiv.style.top = "100px";
movableDiv.style.left = "100px";
movableDiv.style.width = "fit-content";
movableDiv.style.height = "fit-content";
movableDiv.style.backdropFilter = "blur(5px)";
movableDiv.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
movableDiv.style.cursor = "grab";
movableDiv.style.borderRadius = "10px";
movableDiv.style.zIndex = "9990";
const window_name = document.createElement("h3");
window_name.style.color = "white";
window_name.innerText = "На локации:";
window_name.style.marginLeft = "5px";
window_name.style.marginRight = "5px";
window_name.style.marginTop = "5px";
movableDiv.appendChild(window_name);
// Enabling element moving
movableDiv.addEventListener("mousedown", (event) => {
this.is_dragging = true;
this.offset_x = event.clientX - movableDiv.getBoundingClientRect().left;
this.offset_y = event.clientY - movableDiv.getBoundingClientRect().top;
// removing selection while holding
event.preventDefault();
});
document.addEventListener("mousemove", (event) => {
if (this.is_dragging) {
movableDiv.style.left = `${event.clientX - this.offset_x}px`;
movableDiv.style.top = `${event.clientY - this.offset_y}px`;
}
});
document.addEventListener("mouseup", () => {
this.is_dragging = false;
});
// Adding element to page
document.body.appendChild(movableDiv);
return movableDiv;
}
}
//TODO: must be a better way of doing this
const player_lister_tweak = new PlayerLister();
//# sourceMappingURL=main.js.map
})();