// ==UserScript==
// @name JKLM Hardcore Enhanced
// @namespace http://tampermonkey.net/
// @version 2024-08-02
// @license MIT
// @description Enhance JKLM Bomb Party game with customizable difficulty and syllable display
// @author SÜSSWASSERZIERFISCH
// @match https://*.jklm.fun/games/bombparty/
// @icon https://www.google.com/s2/favicons?sz=64&domain=jklm.fun
// @run-at document-start
// @grant GM_xmlhttpRequest
// @connect huggingface.co
// ==/UserScript==
(function() {
'use strict';
// Global variables
let silben_obj = {};
let silben_list = [];
let silben_map = new Map();
let einstellungen = {};
let enabled = false;
let selfID = -1;
let currentModifiedSyllable = "";
let originalSyllable = "";
// Pseudo alphabet mapping
const pseudoAlphabetArray = ["𝖠","𝖡","𝖢","𝖣","𝖤","𝖥","𝖦","𝖧","𝖨","𝖩","𝖪","𝖫","𝖬","𝖭","𝖮","𝖯","𝖰","𝖱","𝖲","𝖳","𝖴","𝖵","𝖶","𝖷","𝖸","𝖹"];
const normalAlphabetArray = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"];
const SILLABLE_URL = 'https://huggingface.co/susswasserzierfisch/silben-modell/resolve/main/silben.tsv';
const DEFAULT_SETTINGS = {
enabled: true,
min: 50,
max: 500,
showSolution: false,
letters: [3, 4],
prepend: true
};
// Convert normal text to pseudo alphabet
function toPseudoAlphabet(text) {
return text.toUpperCase().split('').map(char => {
const index = normalAlphabetArray.indexOf(char);
return index !== -1 ? pseudoAlphabetArray[index] : char;
}).join('');
}
// Simulate typing function
function simulateType(text, isSend) {
if (typeof socket !== 'undefined' && socket.emit) {
socket.emit("setWord", text, isSend);
}
}
// Send chat message function
function sendChatMessage(message) {
if (typeof socket !== 'undefined' && socket.emit) {
socket.emit("chat", message);
}
}
// Helper function to extract only letters, apostrophes, and hyphens
function extractValidChars(text) {
return text.replace(/[^a-zA-Z'-]/g, '').toLowerCase();
}
// Helper function to check if text contains syllable
function containsSyllable(text, syllable) {
const cleanText = extractValidChars(text);
const cleanSyllable = extractValidChars(syllable);
return cleanText.includes(cleanSyllable);
}
// Initialize the script
function init() {
loadSettings();
if (localStorage.silben) {
loadSilben(localStorage.silben);
setupUI();
} else {
fetchSilben();
}
}
// Fetch syllables data from the server
function fetchSilben() {
GM_xmlhttpRequest({
method: 'GET',
url: SILLABLE_URL,
headers: { 'Accept': 'application/json' },
onload: function(response) {
if (response.status === 200) {
try {
localStorage.silben = response.responseText;
loadSilben(localStorage.silben);
setupUI();
} catch (e) {
console.error("Failed to process syllables data:", e);
}
} else {
console.error("Failed to fetch syllables data. Status:", response.status);
}
},
onerror: function(error) {
console.error("Error fetching syllables data:", error);
}
});
}
// Load settings from localStorage
function loadSettings() {
einstellungen = JSON.parse(localStorage.einstellungen || JSON.stringify(DEFAULT_SETTINGS));
enabled = einstellungen.enabled;
}
// Load syllables data and build the syllables map
function loadSilben(silben) {
silben.split("\n").forEach(line => {
let [silbe, count, solution] = line.split("\t");
if (silbe && count) {
silben_obj[silbe] = { count, solution };
silben_list.push([silbe, count]);
}
});
silben_map = buildSilbenMap(silben_list);
}
// Build a map of syllables for quick lookup
function buildSilbenMap(silben_list) {
const silbenMap = new Map();
for (const [silbe, value] of silben_list) {
for (let i = 0; i < silbe.length; i++) {
for (let j = i + 1; j <= silbe.length; j++) {
const subsilbe = silbe.substring(i, j);
if (!silbenMap.has(subsilbe)) {
silbenMap.set(subsilbe, []);
}
silbenMap.get(subsilbe).push([silbe, value]);
}
}
}
return silbenMap;
}
// Set up the user interface
function setupUI() {
if (document.querySelector("#hardcore-settings")) return;
const hardcoreSettings = document.createElement("div");
hardcoreSettings.id = "hardcore-settings";
hardcoreSettings.className = "setting rule hardcoreSettings";
hardcoreSettings.innerHTML = `
<div class="label" data-text="hardcoreMode">⚔️ Hardcore-Addon</div>
<div class="info">Hardcore-Addon Aktivieren?</div>
<div class="field">
<input type="checkbox" id="hardcore-toggle">
</div>
<div class="info">Aktivierte Buchstabenzahlen</div>
<div class="field">
<div class="bonusAlphabetField">
<div class="bonusLetterField">
3 <input type="checkbox" id="hardcore-letter-3">
</div>
<div class="bonusLetterField">
4 <input type="checkbox" id="hardcore-letter-4">
</div>
<div class="bonusLetterField">
5 <input type="checkbox" id="hardcore-letter-5">
</div>
</div>
</div>
<div class="info">Silbenschwierigkeit</div>
<div class="field">
<table>
<tbody>
<tr>
<th>Min:</th>
<td class="range">
<input type="number" id="min-number" min="1" max="50000">
<input type="range" id="min-range" min="1" max="5000">
</td>
</tr>
<tr>
<th>Max:</th>
<td class="range">
<input type="number" id="max-number" min="1" max="50000">
<input type="range" id="max-range" min="1" max="5000">
</td>
</tr>
</tbody>
</table>
</div>
<div class="info">Silbe voranstellen?</div>
<div class="field">
<input type="checkbox" id="prepend-toggle">
</div>`;
// Wait for the rules element to exist
const waitForRules = setInterval(() => {
const rulesElement = document.querySelector(".rules");
if (rulesElement) {
rulesElement.appendChild(hardcoreSettings);
setupEventListeners();
clearInterval(waitForRules);
}
}, 100);
}
// Set up event listeners for UI elements
function setupEventListeners() {
const minNumber = document.querySelector("#min-number");
const minRange = document.querySelector("#min-range");
const maxNumber = document.querySelector("#max-number");
const maxRange = document.querySelector("#max-range");
const toggleCheckbox = document.querySelector("#hardcore-toggle");
const letter3Checkbox = document.querySelector("#hardcore-letter-3");
const letter4Checkbox = document.querySelector("#hardcore-letter-4");
const letter5Checkbox = document.querySelector("#hardcore-letter-5");
const prependCheckbox = document.querySelector("#prepend-toggle");
if (!minNumber || !toggleCheckbox) return;
// Set initial values
minNumber.value = minRange.value = einstellungen.min;
maxNumber.value = maxRange.value = einstellungen.max;
toggleCheckbox.checked = einstellungen.enabled;
letter3Checkbox.checked = einstellungen.letters.includes(3);
letter4Checkbox.checked = einstellungen.letters.includes(4);
letter5Checkbox.checked = einstellungen.letters.includes(5);
prependCheckbox.checked = einstellungen.prepend;
// Event listeners
minNumber.addEventListener("input", () => updateMinMax('min', minNumber, minRange));
minRange.addEventListener("input", () => updateMinMax('min', minRange, minNumber));
maxNumber.addEventListener("input", () => updateMinMax('max', maxNumber, maxRange));
maxRange.addEventListener("input", () => updateMinMax('max', maxRange, maxNumber));
toggleCheckbox.addEventListener("change", () => {
einstellungen.enabled = toggleCheckbox.checked;
enabled = einstellungen.enabled;
updateSettings();
});
letter3Checkbox.addEventListener("change", () => updateLetters(3, letter3Checkbox.checked));
letter4Checkbox.addEventListener("change", () => updateLetters(4, letter4Checkbox.checked));
letter5Checkbox.addEventListener("change", () => updateLetters(5, letter5Checkbox.checked));
prependCheckbox.addEventListener("change", () => {
einstellungen.prepend = prependCheckbox.checked;
updateSettings();
});
}
// Update min/max values in settings
function updateMinMax(type, primary, secondary) {
secondary.value = primary.value;
einstellungen[type] = parseInt(primary.value, 10);
updateSettings();
}
// Update enabled letters in settings
function updateLetters(letter, isChecked) {
if (isChecked) {
if (!einstellungen.letters.includes(letter)) {
einstellungen.letters.push(letter);
}
} else {
einstellungen.letters = einstellungen.letters.filter(l => l !== letter);
}
updateSettings();
}
// Save settings to localStorage
function updateSettings() {
localStorage.einstellungen = JSON.stringify(einstellungen);
}
// Choose a syllable based on enabled letters and difficulty range
function silbeWahlen(subsilbe) {
if (!enabled) return subsilbe;
const candidates = (silben_map.get(subsilbe) || []).filter(silbe => {
const syllableLength = silbe[0].length;
const syllableValue = parseInt(silbe[1], 10);
return einstellungen.letters.includes(syllableLength) &&
syllableValue >= einstellungen.min &&
syllableValue <= einstellungen.max;
});
if (candidates.length > 0) {
let randomIndex = Math.floor(Math.random() * candidates.length);
return candidates[randomIndex][0];
}
return subsilbe;
}
// Utility function to split the incoming string
function splitString(input) {
const regex = /^(\d+)(.*)/;
const match = input.match(regex);
return match ? { digits: match[1], rest: match[2] } : { digits: null, rest: input };
}
function modifySend(data) {
try {
let { digits, rest } = splitString(data);
// Check if there's actually JSON data to parse
if (!rest || rest.length === 0) {
return data;
}
let json = JSON.parse(rest);
// Check if this is a setWord message
if (data.includes("setWord") && json.length >= 3) {
console.log("HELLO", data);
console.log("Original JSON:", json);
// The structure is ["setWord", word, isSend]
let userInput = json[1]; // The word is at index 1
let isSend = json[2]; // The boolean is at index 2
// Fix 2: Don't modify if input starts with '/'
if (userInput && typeof userInput === 'string' && userInput.startsWith('/')) {
return data;
}
// Updated logic: Prevent sending if word contains original syllable but not modified one
if (userInput && typeof userInput === 'string' && originalSyllable && currentModifiedSyllable) {
const containsOriginal = containsSyllable(userInput, originalSyllable);
const containsModified = containsSyllable(userInput, currentModifiedSyllable);
// If word contains original syllable but not the modified one, prevent sending
if (containsOriginal && !containsModified) {
json[2] = false; // Set isSend to false
}
// If word doesn't contain original syllable at all, allow sending (don't change isSend)
// If word contains modified syllable, allow sending (don't change isSend)
}
// Check if userInput exists and currentModifiedSyllable is valid
if (userInput &&
typeof userInput === 'string' &&
userInput.length < 30 &&
currentModifiedSyllable &&
currentModifiedSyllable !== originalSyllable &&
currentModifiedSyllable.trim() !== '' &&
enabled &&
einstellungen.prepend) {
let pseudoSyllable = toPseudoAlphabet(currentModifiedSyllable.toUpperCase());
let modifiedInput = `(${pseudoSyllable}) ${userInput}`;
// Only prepend the syllable if the final result is 30 characters or less
if (modifiedInput.length <= 30) {
json[1] = modifiedInput;
console.log("MODIFIED!", json);
return digits + JSON.stringify(json);
}
}
}
} catch (err) {
console.log("Failed to modify send:", err);
}
// Always return the original data if no modification was made
return data;
}
// Modify incoming string messages after receiving
function modifyReceive(data) {
let { digits, rest } = splitString(data);
// Check if there's actually JSON data to parse
if (!rest || rest.length === 0) {
return data;
}
try {
let json = JSON.parse(rest);
if (data.includes("nextTurn") && json.length >= 3) {
let [, targetUser, newSilbe] = json;
originalSyllable = newSilbe;
if (targetUser === selfID) {
currentModifiedSyllable = silbeWahlen(newSilbe.toUpperCase()).toLowerCase();
// Send the modified syllable in pseudo alphabet immediately only if enabled and prepend is on
if (currentModifiedSyllable !== originalSyllable && enabled && einstellungen.prepend) {
let pseudoSyllable = toPseudoAlphabet(currentModifiedSyllable.toUpperCase());
setTimeout(() => {
sendChatMessage(`(${pseudoSyllable})`);
}, 100);
}
// Fix 3: Automatically write the syllable when turn starts only if enabled and prepend is on
if (enabled && einstellungen.prepend) {
setTimeout(() => {
simulateType(currentModifiedSyllable, false);
}, 150);
}
newSilbe = currentModifiedSyllable;
}
json[2] = newSilbe;
return digits + JSON.stringify(json);
}
if (data.includes("syllable") && json.length >= 2 && json[1] && json[1].currentPlayerPeerId !== undefined) {
let targetUser = json[1].currentPlayerPeerId;
let newSilbe = json[1].syllable;
originalSyllable = newSilbe;
if (targetUser === selfID) {
currentModifiedSyllable = silbeWahlen(newSilbe.toUpperCase()).toLowerCase();
// Send the modified syllable in pseudo alphabet immediately only if enabled and prepend is on
if (currentModifiedSyllable !== originalSyllable && enabled && einstellungen.prepend) {
let pseudoSyllable = toPseudoAlphabet(currentModifiedSyllable.toUpperCase());
setTimeout(() => {
sendChatMessage(`(${pseudoSyllable})`);
}, 100);
}
// Fix 3: Automatically write the syllable when turn starts only if enabled and prepend is on
if (enabled && einstellungen.prepend) {
setTimeout(() => {
simulateType(currentModifiedSyllable, false);
}, 150);
}
json[1].syllable = currentModifiedSyllable;
}
return digits + JSON.stringify(json);
}
if (data.includes("selfPeerId") && json.length >= 2 && json[1] && json[1].selfPeerId !== undefined) {
selfID = json[1].selfPeerId;
}
} catch (err) {
console.log("Failed to parse JSON:", err);
}
return data;
}
// Override the WebSocket send method
const originalSend = WebSocket.prototype.send;
WebSocket.prototype.send = function(...args) {
let [dataToSend] = args;
if (typeof dataToSend === 'string') {
console.log("[WebSocket Interceptor] Sending:", dataToSend);
dataToSend = modifySend(dataToSend);
args[0] = dataToSend;
}
return originalSend.apply(this, args);
};
// Intercept incoming WebSocket messages
function interceptWebSocketMessages() {
let originalDescriptor = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
Object.defineProperty(MessageEvent.prototype, "data", {
get: function() {
let originalData = originalDescriptor.get.call(this);
if (this.currentTarget instanceof WebSocket && typeof originalData === 'string') {
console.log("[WebSocket Interceptor] Received:", originalData);
return modifyReceive(originalData);
}
return originalData;
}
});
}
// Initialize the script
init();
interceptWebSocketMessages();
})();