Bomb Party Suggester

Suggests words containing the required syllable for JKLM.fun Bomb Party game with customizable dictionaries and sorting options

// ==UserScript==
// @name         Bomb Party Suggester
// @namespace    http://tampermonkey.net/
// @version      0.1.2
// @description  Suggests words containing the required syllable for JKLM.fun Bomb Party game with customizable dictionaries and sorting options
// @author       Doomsy1
// @match        *.jklm.fun/games/bombparty*
// @grant        none
// @run-at       document-start
// @supportURL   https://github.com/Doomsy1/Bomb-Party-Suggester/issues
// @icon         https://www.google.com/s2/favicons?sz=64&domain=jklm.fun
// @license      MIT
// ==/UserScript==
(() => {
  // src/core/typer.js
  window.BPS = window.BPS || {};
  (function() {
    let KEYBOARD_LAYOUT = {
      layout: {
        q: [0, 0],
        w: [0, 1],
        e: [0, 2],
        r: [0, 3],
        t: [0, 4],
        y: [0, 5],
        u: [0, 6],
        i: [0, 7],
        o: [0, 8],
        p: [0, 9],
        a: [1, 0],
        s: [1, 1],
        d: [1, 2],
        f: [1, 3],
        g: [1, 4],
        h: [1, 5],
        j: [1, 6],
        k: [1, 7],
        l: [1, 8],
        z: [2, 0],
        x: [2, 1],
        c: [2, 2],
        v: [2, 3],
        b: [2, 4],
        n: [2, 5],
        m: [2, 6]
      },
      adjacent: {}
    };
    Object.entries(KEYBOARD_LAYOUT.layout).forEach(([key, [row, col]]) => {
      KEYBOARD_LAYOUT.adjacent[key] = Object.entries(KEYBOARD_LAYOUT.layout).filter(([k, [r, c]]) => {
        if (k === key) return !1;
        let rowDiff = Math.abs(r - row), colDiff = Math.abs(c - col);
        return rowDiff <= 1 && colDiff <= 1;
      }).map(([k]) => k);
    });
    let TYPER_CONFIG = {
      baseDelay: 60,
      distanceMultiplier: 12.5,
      minDelay: 15,
      delayVariation: 0.2,
      typoChance: 2,
      typoNoticeDelay: { mean: 250, stdDev: 60 },
      typoBackspaceDelay: { mean: 100, stdDev: 40 },
      typoRecoveryDelay: { mean: 200, stdDev: 50 }
    };
    function loadSavedSettings() {
      let saved = localStorage.getItem("bombPartyTyperSettings");
      if (saved)
        try {
          let parsed = JSON.parse(saved);
          Object.assign(TYPER_CONFIG, parsed);
        } catch {
        }
    }
    function saveSettings() {
      try {
        localStorage.setItem("bombPartyTyperSettings", JSON.stringify(TYPER_CONFIG));
      } catch {
      }
    }
    function normalRandom(mean, stdDev) {
      let u = 0, v = 0;
      for (; u === 0; ) u = Math.random();
      for (; v === 0; ) v = Math.random();
      let num = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
      return Math.floor(num * stdDev + mean);
    }
    function calculateTypingDelay(fromKey, toKey) {
      if (!fromKey) return TYPER_CONFIG.baseDelay;
      fromKey = fromKey.toLowerCase(), toKey = toKey.toLowerCase();
      let fromPos = KEYBOARD_LAYOUT.layout[fromKey], toPos = KEYBOARD_LAYOUT.layout[toKey];
      if (!fromPos || !toPos) return TYPER_CONFIG.baseDelay;
      let distance = Math.sqrt(
        Math.pow(fromPos[0] - toPos[0], 2) + Math.pow(fromPos[1] - toPos[1], 2)
      ), meanDelay = TYPER_CONFIG.baseDelay + distance * TYPER_CONFIG.distanceMultiplier, stdDev = meanDelay * TYPER_CONFIG.delayVariation;
      return Math.max(TYPER_CONFIG.minDelay, normalRandom(meanDelay, stdDev));
    }
    async function simulateTypo(inputField, correctChar) {
      let c = correctChar.toLowerCase();
      if (!KEYBOARD_LAYOUT.adjacent[c] || Math.random() > TYPER_CONFIG.typoChance / 100) return !1;
      let neighbors = KEYBOARD_LAYOUT.adjacent[c], typoChar = neighbors[Math.floor(Math.random() * neighbors.length)];
      return inputField.value += typoChar, inputField.dispatchEvent(new Event("input", { bubbles: !0 })), await new Promise((resolve) => setTimeout(resolve, calculateTypingDelay(null, typoChar))), await new Promise((resolve) => setTimeout(
        resolve,
        normalRandom(TYPER_CONFIG.typoNoticeDelay.mean, TYPER_CONFIG.typoNoticeDelay.stdDev)
      )), inputField.value = inputField.value.slice(0, -1), inputField.dispatchEvent(new Event("input", { bubbles: !0 })), await new Promise((resolve) => setTimeout(
        resolve,
        normalRandom(TYPER_CONFIG.typoBackspaceDelay.mean, TYPER_CONFIG.typoBackspaceDelay.stdDev)
      )), inputField.value += correctChar, inputField.dispatchEvent(new Event("input", { bubbles: !0 })), await new Promise((resolve) => setTimeout(
        resolve,
        normalRandom(TYPER_CONFIG.typoRecoveryDelay.mean, TYPER_CONFIG.typoRecoveryDelay.stdDev)
      )), !0;
    }
    async function simulateTyping(word) {
      let selfTurn = document.querySelector(".selfTurn"), form = document.querySelector(".selfTurn form"), inputField = document.querySelector(".selfTurn input");
      if (!inputField || !form || selfTurn.hidden)
        return;
      inputField.value = "", inputField.focus();
      let lastChar = null;
      for (let i = 0; i < word.length; i++)
        await simulateTypo(inputField, word[i]) || (inputField.value += word[i], inputField.dispatchEvent(new Event("input", { bubbles: !0 })), await new Promise((resolve) => setTimeout(resolve, calculateTypingDelay(lastChar, word[i]))), lastChar = word[i]);
      form.dispatchEvent(new Event("submit", { bubbles: !0, cancelable: !0 }));
    }
    function isPlayerTurn() {
      let selfTurn = document.querySelector(".selfTurn");
      return selfTurn && !selfTurn.hidden;
    }
    loadSavedSettings(), window.BPS.KEYBOARD_LAYOUT = KEYBOARD_LAYOUT, window.BPS.TYPER_CONFIG = TYPER_CONFIG, window.BPS.loadSavedSettings = loadSavedSettings, window.BPS.saveSettings = saveSettings, window.BPS.normalRandom = normalRandom, window.BPS.calculateTypingDelay = calculateTypingDelay, window.BPS.simulateTyping = simulateTyping, window.BPS.isPlayerTurn = isPlayerTurn;
  })();

  // src/core/dictionaryLoader.js
  window.BPS = window.BPS || {};
  (function() {
    let dictionaries = {
      "5k": {
        url: "https://raw.githubusercontent.com/filiph/english_words/master/data/word-freq-top5000.csv",
        words: [],
        hasFrequency: !0
      },
      "20k": {
        url: "https://raw.githubusercontent.com/first20hours/google-10000-english/master/google-10000-english-usa.txt",
        words: [],
        hasFrequency: !0
      },
      "273k": {
        url: "https://raw.githubusercontent.com/kli512/bombparty-assist/refs/heads/main/bombparty/dictionaries/en.txt",
        words: [],
        hasFrequency: !1
      }
    };
    async function loadDictionary(size) {
      let dictionary = dictionaries[size], lines = (await (await fetch(dictionary.url)).text()).split(`
`);
      switch (size) {
        case "5k":
          let dataLines = lines.slice(1);
          dictionary.words = dataLines.map((line) => {
            let trimmed = line.trim();
            if (!trimmed) return { word: "", freq: 0 };
            let parts = trimmed.split(",");
            if (parts.length < 4) return { word: "", freq: 0 };
            let word = parts[1] || "", freq = parseInt(parts[3], 10) || 0;
            return { word, freq };
          });
          break;
        case "20k":
          dictionary.words = lines.map((line, idx) => ({
            word: line.trim(),
            freq: lines.length - idx
            // higher index = less frequent
          }));
          break;
        case "273k":
          dictionary.words = lines.filter((line) => line.trim().length > 0).map((line) => ({
            word: line.trim().toLowerCase(),
            freq: 1
            // treat all words equally
          }));
          break;
        default:
          return;
      }
      dictionary.words = dictionary.words.filter((entry) => entry.word);
    }
    async function loadAllDictionaries() {
      try {
        await Promise.all([
          loadDictionary("5k"),
          loadDictionary("20k"),
          loadDictionary("273k")
        ]);
      } catch {
      }
    }
    window.BPS.dictionaries = dictionaries, window.BPS.loadAllDictionaries = loadAllDictionaries;
  })();

  // src/ui/styles.js
  window.BPS = window.BPS || {};
  (function() {
    let styles = {
      colors: {
        primary: "#61dafb",
        background: "#282c34",
        text: "#ffffff",
        highlight: "#2EFF2E",
        special: "#FF8C00"
      },
      panel: {
        position: "fixed",
        top: "10px",
        right: "10px",
        backgroundColor: "rgba(40, 44, 52, 0.5)",
        border: "2px solid #61dafb",
        borderRadius: "8px",
        padding: "10px",
        zIndex: "2147483647",
        maxWidth: "500px",
        minWidth: "200px",
        minHeight: "150px",
        maxHeight: "800px",
        width: "300px",
        height: "400px",
        fontFamily: "sans-serif",
        fontSize: "14px",
        color: "#fff",
        boxShadow: "0px 4px 12px rgba(0,0,0,0.5)",
        cursor: "move",
        resize: "none",
        overflow: "hidden"
      },
      resizeHandle: {
        position: "absolute",
        width: "20px",
        height: "20px",
        background: "transparent",
        zIndex: "2147483647",
        cursor: "nw-resize"
      },
      resizeDot: {
        position: "absolute",
        width: "8px",
        height: "8px",
        background: "#61dafb",
        borderRadius: "50%",
        left: "50%",
        top: "50%",
        transform: "translate(-50%, -50%)"
      },
      resizeEdge: {
        position: "absolute",
        background: "transparent",
        zIndex: "2147483647"
      },
      sizeSelector: {
        marginBottom: "4px",
        display: "flex",
        gap: "8px",
        justifyContent: "center"
      },
      sortControls: {
        marginBottom: "8px",
        display: "flex",
        gap: "8px",
        justifyContent: "center",
        flexWrap: "wrap"
      },
      sortButton: {
        padding: "4px 8px",
        border: "1px solid #61dafb",
        borderRadius: "4px",
        background: "transparent",
        color: "#fff",
        cursor: "pointer",
        fontSize: "12px",
        display: "flex",
        alignItems: "center",
        gap: "4px"
      },
      activeSortButton: {
        background: "#61dafb",
        color: "#282c34"
      },
      button: {
        padding: "4px 8px",
        border: "1px solid #61dafb",
        borderRadius: "4px",
        background: "transparent",
        color: "#fff",
        cursor: "pointer"
      },
      activeButton: {
        background: "#61dafb",
        color: "#282c34"
      },
      resultsList: {
        listStyle: "none",
        padding: "0",
        margin: "0"
      },
      resultsItem: {
        padding: "4px 0",
        textAlign: "center",
        fontSize: "14px",
        cursor: "pointer",
        transition: "background-color 0.2s",
        borderRadius: "4px"
      },
      resultsItemHover: {
        backgroundColor: "rgba(97, 218, 251, 0.2)"
      },
      resultsItemDisabled: {
        backgroundColor: "rgba(220, 53, 69, 0.2)"
      },
      resultsDiv: {
        height: "auto",
        overflowY: "visible",
        marginTop: "8px"
      },
      settingsButton: {
        position: "absolute",
        top: "10px",
        right: "10px",
        padding: "4px 8px",
        border: "1px solid #61dafb",
        borderRadius: "4px",
        background: "transparent",
        color: "#fff",
        cursor: "pointer",
        fontSize: "12px"
      },
      settingsPanel: {
        position: "fixed",
        top: "10px",
        left: "10px",
        backgroundColor: "rgba(40, 44, 52, 0.9)",
        border: "2px solid #61dafb",
        borderRadius: "8px",
        padding: "12px",
        zIndex: "2147483647",
        width: "220px",
        color: "#fff",
        fontFamily: "sans-serif",
        fontSize: "12px",
        cursor: "move",
        boxShadow: "0px 4px 12px rgba(0,0,0,0.5)"
      },
      settingsGroup: {
        marginBottom: "8px",
        display: "flex",
        flexDirection: "column"
      },
      settingsLabel: {
        display: "block",
        marginBottom: "2px",
        color: "#61dafb",
        fontSize: "11px"
      },
      settingsInputGroup: {
        display: "flex",
        gap: "8px",
        alignItems: "center"
      },
      settingsInput: {
        width: "50px",
        padding: "2px 4px",
        backgroundColor: "rgba(255, 255, 255, 0.1)",
        border: "1px solid #61dafb",
        borderRadius: "4px",
        color: "#fff",
        fontSize: "11px"
      },
      settingsSlider: {
        flex: 1,
        height: "4px",
        WebkitAppearance: "none",
        background: "rgba(97, 218, 251, 0.2)",
        borderRadius: "2px",
        outline: "none"
      }
    };
    function applyStyles(element, styleObj) {
      Object.assign(element.style, styleObj);
    }
    window.BPS.styles = styles, window.BPS.applyStyles = applyStyles;
  })();

  // src/ui/dragResize.js
  window.BPS = window.BPS || {};
  (function() {
    "use strict";
    let styles = window.BPS.styles, applyStyles = window.BPS.applyStyles;
    function makeDraggable(element) {
      let isDragging = !1, offsetX = 0, offsetY = 0;
      element.addEventListener("mousedown", (e) => {
        let tag = e.target.tagName.toLowerCase();
        tag === "button" || tag === "input" || (isDragging = !0, offsetX = e.clientX - element.offsetLeft, offsetY = e.clientY - element.offsetTop, e.preventDefault());
      }), document.addEventListener("mousemove", (e) => {
        if (!isDragging) return;
        e.preventDefault();
        let x = e.clientX - offsetX, y = e.clientY - offsetY;
        element.style.left = Math.max(0, Math.min(window.innerWidth - element.offsetWidth, x)) + "px", element.style.top = Math.max(0, Math.min(window.innerHeight - element.offsetHeight, y)) + "px";
      }), document.addEventListener("mouseup", () => {
        isDragging = !1;
      });
    }
    function setupDraggableResize(panel) {
      [
        { corner: "nw", top: "-10px", left: "-10px", cursor: "nw-resize" },
        { corner: "ne", top: "-10px", right: "-10px", cursor: "ne-resize" },
        { corner: "se", bottom: "-10px", right: "-10px", cursor: "se-resize" },
        { corner: "sw", bottom: "-10px", left: "-10px", cursor: "sw-resize" }
      ].forEach((pos) => {
        let handle = document.createElement("div");
        handle.className = `resize-handle ${pos.corner}`, applyStyles(handle, { ...styles.resizeHandle, ...pos });
        let dot = document.createElement("div");
        applyStyles(dot, styles.resizeDot), handle.appendChild(dot), panel.appendChild(handle);
      }), [
        { edge: "n", top: "-5px", left: "20px", right: "20px", height: "10px", cursor: "ns-resize" },
        { edge: "s", bottom: "-5px", left: "20px", right: "20px", height: "10px", cursor: "ns-resize" },
        { edge: "e", top: "20px", right: "-5px", bottom: "20px", width: "10px", cursor: "ew-resize" },
        { edge: "w", top: "20px", left: "-5px", bottom: "20px", width: "10px", cursor: "ew-resize" }
      ].forEach((pos) => {
        let edge = document.createElement("div");
        edge.className = `resize-edge ${pos.edge}`, applyStyles(edge, { ...styles.resizeEdge, ...pos }), panel.appendChild(edge);
      });
      let draggingPanel = !1, offsetX = 0, offsetY = 0;
      panel.addEventListener("mousedown", (e) => {
        e.target.classList.contains("resize-handle") || e.target.classList.contains("resize-edge") || (draggingPanel = !0, offsetX = e.clientX - panel.getBoundingClientRect().left, offsetY = e.clientY - panel.getBoundingClientRect().top, e.preventDefault());
      }), panel.addEventListener("mousemove", (e) => {
        if (!draggingPanel) return;
        let newLeft = e.clientX - offsetX, newTop = e.clientY - offsetY;
        panel.style.left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, newLeft)) + "px", panel.style.top = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, newTop)) + "px";
      }), panel.addEventListener("mouseup", () => {
        draggingPanel = !1;
      }), panel.addEventListener("mouseleave", () => {
        draggingPanel = !1;
      });
      let resizing = !1, currentResizer = null, startX, startY, startWidth, startHeight, panelLeft, panelTop;
      [
        ...panel.querySelectorAll(".resize-handle"),
        ...panel.querySelectorAll(".resize-edge")
      ].forEach((r) => {
        r.addEventListener("mousedown", (e) => {
          resizing = !0, currentResizer = r, startX = e.clientX, startY = e.clientY;
          let rect = panel.getBoundingClientRect();
          startWidth = rect.width, startHeight = rect.height, panelLeft = rect.left, panelTop = rect.top, e.preventDefault(), e.stopPropagation();
        });
      }), document.addEventListener("mousemove", (e) => {
        if (!resizing || !currentResizer) return;
        let dx = e.clientX - startX, dy = e.clientY - startY, maxW = parseInt(styles.panel.maxWidth, 10) || 500, minW = parseInt(styles.panel.minWidth, 10) || 200, maxH = parseInt(styles.panel.maxHeight, 10) || 800, minH = parseInt(styles.panel.minHeight, 10) || 150, newW = startWidth, newH = startHeight, newL = panelLeft, newT = panelTop, direction = currentResizer.classList[1];
        if (currentResizer.classList.contains("resize-handle"))
          switch (direction) {
            case "nw":
              newW = startWidth - dx, newH = startHeight - dy, newL = panelLeft + (startWidth - newW), newT = panelTop + (startHeight - newH);
              break;
            case "ne":
              newW = startWidth + dx, newH = startHeight - dy, newT = panelTop + (startHeight - newH);
              break;
            case "se":
              newW = startWidth + dx, newH = startHeight + dy;
              break;
            case "sw":
              newW = startWidth - dx, newH = startHeight + dy, newL = panelLeft + (startWidth - newW);
              break;
          }
        else
          switch (direction) {
            case "n":
              newH = startHeight - dy, newT = panelTop + (startHeight - newH);
              break;
            case "s":
              newH = startHeight + dy;
              break;
            case "e":
              newW = startWidth + dx;
              break;
            case "w":
              newW = startWidth - dx, newL = panelLeft + (startWidth - newW);
              break;
          }
        newW = Math.min(maxW, Math.max(minW, newW)), newH = Math.min(maxH, Math.max(minH, newH)), newL = Math.min(window.innerWidth - newW, Math.max(0, newL)), newT = Math.min(window.innerHeight - newH, Math.max(0, newT)), panel.style.width = newW + "px", panel.style.height = newH + "px", panel.style.left = newL + "px", panel.style.top = newT + "px";
      }), document.addEventListener("mouseup", () => {
        resizing = !1, currentResizer = null;
      });
    }
    window.BPS.makeDraggable = makeDraggable, window.BPS.setupDraggableResize = setupDraggableResize;
  })();

  // src/ui/settings.js
  window.BPS = window.BPS || {};
  (function() {
    "use strict";
    let styles = window.BPS.styles, applyStyles = window.BPS.applyStyles, TYPER_CONFIG = window.BPS.TYPER_CONFIG, saveSettings = window.BPS.saveSettings, loadSavedSettings = window.BPS.loadSavedSettings, makeDraggable = window.BPS.makeDraggable;
    function createSettingsPanel() {
      let panel = document.createElement("div");
      panel.id = "typerSettingsPanel", applyStyles(panel, styles.settingsPanel), panel.style.display = "none", makeDraggable(panel);
      let header = document.createElement("div");
      applyStyles(header, {
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        marginBottom: "10px"
      });
      let title = document.createElement("h3");
      title.textContent = "Typer Settings", title.style.margin = "0", title.style.color = "#61dafb", title.style.fontSize = "14px", header.appendChild(title);
      let resetBtn = document.createElement("button");
      return resetBtn.textContent = "\u21BA", resetBtn.title = "Reset to defaults", applyStyles(resetBtn, {
        ...styles.button,
        padding: "2px 6px",
        fontSize: "14px",
        marginLeft: "8px",
        backgroundColor: "transparent"
      }), resetBtn.onmouseenter = () => {
        resetBtn.style.backgroundColor = "rgba(97, 218, 251, 0.2)";
      }, resetBtn.onmouseleave = () => {
        resetBtn.style.backgroundColor = "transparent";
      }, resetBtn.onclick = () => {
        Object.assign(TYPER_CONFIG, JSON.parse(JSON.stringify({
          baseDelay: 60,
          distanceMultiplier: 12.5,
          minDelay: 15,
          delayVariation: 0.2,
          typoChance: 2,
          typoNoticeDelay: { mean: 250, stdDev: 60 },
          typoBackspaceDelay: { mean: 100, stdDev: 40 },
          typoRecoveryDelay: { mean: 200, stdDev: 50 }
        }))), saveSettings(), refreshSettingsPanel(panel);
      }, header.appendChild(resetBtn), panel.appendChild(header), panel.appendChild(createSettingInput("Base Delay (ms)", "baseDelay", TYPER_CONFIG.baseDelay, 0, 100, 1)), panel.appendChild(createSettingInput("Distance Multiplier", "distanceMultiplier", TYPER_CONFIG.distanceMultiplier, 0, 20, 0.1)), panel.appendChild(createSettingInput("Minimum Delay (ms)", "minDelay", TYPER_CONFIG.minDelay, 0, 50, 1)), panel.appendChild(createSettingInput("Delay Variation", "delayVariation", TYPER_CONFIG.delayVariation, 0, 1, 0.01)), panel.appendChild(createSettingInput("Typo Chance (%)", "typoChance", TYPER_CONFIG.typoChance, 0, 10, 0.1)), panel.appendChild(createSettingInput("Notice Delay (ms)", "typoNoticeDelay.mean", TYPER_CONFIG.typoNoticeDelay.mean, 0, 1e3, 10)), panel.appendChild(createSettingInput("Notice Variation", "typoNoticeDelay.stdDev", TYPER_CONFIG.typoNoticeDelay.stdDev, 0, 200, 5)), panel.appendChild(createSettingInput("Backspace Delay (ms)", "typoBackspaceDelay.mean", TYPER_CONFIG.typoBackspaceDelay.mean, 0, 500, 10)), panel.appendChild(createSettingInput("Backspace Variation", "typoBackspaceDelay.stdDev", TYPER_CONFIG.typoBackspaceDelay.stdDev, 0, 100, 5)), panel.appendChild(createSettingInput("Recovery Delay (ms)", "typoRecoveryDelay.mean", TYPER_CONFIG.typoRecoveryDelay.mean, 0, 500, 10)), panel.appendChild(createSettingInput("Recovery Variation", "typoRecoveryDelay.stdDev", TYPER_CONFIG.typoRecoveryDelay.stdDev, 0, 100, 5)), document.body.appendChild(panel), panel;
    }
    function createSettingInput(labelText, configPath, initialValue, min, max, step) {
      let group = document.createElement("div");
      group.className = "settingsGroup", applyStyles(group, styles.settingsGroup);
      let labelEl = document.createElement("label");
      labelEl.textContent = labelText, applyStyles(labelEl, styles.settingsLabel), group.appendChild(labelEl);
      let inputGroup = document.createElement("div");
      applyStyles(inputGroup, styles.settingsInputGroup);
      let slider = document.createElement("input");
      slider.type = "range", slider.min = min, slider.max = max, slider.step = step, slider.value = initialValue, applyStyles(slider, styles.settingsSlider);
      let numericInput = document.createElement("input");
      numericInput.type = "number", numericInput.value = initialValue, numericInput.min = min, numericInput.max = max, numericInput.step = step, applyStyles(numericInput, styles.settingsInput);
      let updateValue = (val) => {
        let keys = configPath.split("."), target = TYPER_CONFIG;
        for (let i = 0; i < keys.length - 1; i++)
          target = target[keys[i]];
        target[keys[keys.length - 1]] = parseFloat(val), slider.value = val, numericInput.value = val, saveSettings();
      };
      return slider.addEventListener("input", () => updateValue(slider.value)), numericInput.addEventListener("change", () => updateValue(numericInput.value)), inputGroup.appendChild(slider), inputGroup.appendChild(numericInput), group.appendChild(inputGroup), group;
    }
    function refreshSettingsPanel(panel) {
      panel.querySelectorAll(".settingsGroup").forEach((group) => {
        let label = group.querySelector("label").textContent, slider = group.querySelector('input[type="range"]'), numericInput = group.querySelector('input[type="number"]'), path = {
          "Base Delay (ms)": "baseDelay",
          "Distance Multiplier": "distanceMultiplier",
          "Minimum Delay (ms)": "minDelay",
          "Delay Variation": "delayVariation",
          "Typo Chance (%)": "typoChance",
          "Notice Delay (ms)": "typoNoticeDelay.mean",
          "Notice Variation": "typoNoticeDelay.stdDev",
          "Backspace Delay (ms)": "typoBackspaceDelay.mean",
          "Backspace Variation": "typoBackspaceDelay.stdDev",
          "Recovery Delay (ms)": "typoRecoveryDelay.mean",
          "Recovery Variation": "typoRecoveryDelay.stdDev"
        }[label];
        if (!path) return;
        let parts = path.split("."), val = TYPER_CONFIG;
        for (let p of parts)
          val = val[p];
        slider.value = val, numericInput.value = val;
      });
    }
    window.BPS.createSettingsPanel = createSettingsPanel, window.BPS.refreshSettingsPanel = refreshSettingsPanel;
  })();

  // src/ui/suggester.js
  window.BPS = window.BPS || {};
  (function() {
    "use strict";
    let styles = window.BPS.styles, applyStyles = window.BPS.applyStyles, dictionaries = window.BPS.dictionaries, simulateTyping = window.BPS.simulateTyping, isPlayerTurn = window.BPS.isPlayerTurn, currentDictionary = "20k", currentSort = { method: "frequency", direction: "desc" }, letterScores = {
      e: 1,
      t: 2,
      a: 3,
      o: 4,
      i: 5,
      n: 6,
      s: 7,
      r: 8,
      h: 9,
      d: 10,
      l: 11,
      u: 12,
      c: 13,
      m: 14,
      f: 15,
      y: 16,
      w: 17,
      g: 18,
      p: 19,
      b: 20,
      v: 21,
      k: 22,
      x: 23,
      q: 24,
      j: 25,
      z: 26
    };
    function calculateRarityScore(word) {
      return word.toLowerCase().split("").reduce((score, letter) => score + (letterScores[letter] || 13), 0);
    }
    function sortMatches(matches) {
      let { method, direction } = currentSort;
      method === "frequency" && !dictionaries[currentDictionary].hasFrequency && (method = "length");
      let sortFns = {
        frequency: (a, b) => b.freq - a.freq,
        length: (a, b) => b.word.length - a.word.length,
        rarity: (a, b) => calculateRarityScore(b.word) - calculateRarityScore(a.word)
      }, sortFn = sortFns[method] || sortFns.length;
      return matches.sort(direction === "desc" ? sortFn : (a, b) => -sortFn(a, b)), matches;
    }
    function suggestWords(syllable) {
      let resultsDiv = document.getElementById("bombPartyWordSuggesterResults");
      if (!resultsDiv) return;
      if (!syllable) {
        resultsDiv.textContent = "(Waiting for syllable...)";
        return;
      }
      let dictObj = dictionaries[currentDictionary];
      if (!dictObj.words.length) {
        resultsDiv.textContent = "Dictionary not ready yet...";
        return;
      }
      let lower = syllable.toLowerCase(), matches = dictObj.words.filter((e) => e.word.toLowerCase().includes(lower));
      if (!matches.length) {
        resultsDiv.textContent = "No suggestions found.";
        return;
      }
      sortMatches(matches);
      let ul = document.createElement("ul");
      applyStyles(ul, styles.resultsList), matches.slice(0, 15).forEach(({ word }) => {
        let li = document.createElement("li");
        applyStyles(li, styles.resultsItem), li.onmouseenter = () => {
          isPlayerTurn() ? applyStyles(li, styles.resultsItemHover) : applyStyles(li, styles.resultsItemDisabled);
        }, li.onmouseleave = () => {
          applyStyles(li, { backgroundColor: "transparent" });
        }, li.onclick = () => {
          isPlayerTurn() && simulateTyping(word);
        };
        let idx = word.toLowerCase().indexOf(lower);
        if (idx >= 0) {
          let before = word.slice(0, idx), match = word.slice(idx, idx + lower.length), after = word.slice(idx + lower.length);
          li.innerHTML = `${before}<span style="color:${styles.colors.highlight}">${match}</span>${after}`;
        } else
          li.textContent = word;
        ul.appendChild(li);
      }), resultsDiv.innerHTML = "", resultsDiv.appendChild(ul);
    }
    function createDictionarySizeSelector() {
      let container = document.createElement("div");
      return applyStyles(container, styles.sizeSelector), ["5k", "20k", "273k"].forEach((dictSize) => {
        let btn = document.createElement("button");
        btn.textContent = dictSize, applyStyles(btn, styles.button), btn.onclick = () => {
          if (!dictionaries[dictSize].words.length) return;
          currentDictionary = dictSize, [...container.querySelectorAll("button")].forEach((b) => {
            applyStyles(b, styles.button);
          }), applyStyles(btn, { ...styles.button, ...styles.activeButton }), currentSort.method === "frequency" && !dictionaries[dictSize].hasFrequency && (currentSort.method = "length", currentSort.direction = "desc");
          let sEl = document.querySelector(".syllable");
          sEl && suggestWords(sEl.textContent.trim());
        }, btn.onmousedown = (e) => e.stopPropagation(), container.appendChild(btn);
      }), container;
    }
    function createSortControls() {
      let sortControls = document.createElement("div");
      return applyStyles(sortControls, styles.sortControls), Object.entries({
        frequency: "Freq",
        length: "Len",
        rarity: "Rare"
      }).forEach(([method, label]) => {
        let btn = document.createElement("button");
        btn.textContent = label + " \u2191", applyStyles(btn, styles.sortButton);
        let isAscending = !0;
        btn.onclick = () => {
          if (method === "frequency" && !dictionaries[currentDictionary].hasFrequency)
            return;
          currentSort.method === method ? isAscending = !isAscending : isAscending = !0, currentSort.method = method, currentSort.direction = isAscending ? "desc" : "asc", [...sortControls.querySelectorAll("button")].forEach((b) => {
            applyStyles(b, styles.sortButton), b.textContent = b.textContent.replace(/[↑↓]/, "\u2191");
          }), applyStyles(btn, { ...styles.sortButton, ...styles.activeSortButton }), btn.textContent = `${label} ${isAscending ? "\u2193" : "\u2191"}`;
          let sEl = document.querySelector(".syllable");
          sEl && suggestWords(sEl.textContent.trim());
        }, btn.onmousedown = (e) => e.stopPropagation(), sortControls.appendChild(btn);
      }), sortControls;
    }
    function getCurrentDictionary() {
      return currentDictionary;
    }
    function getCurrentSort() {
      return currentSort;
    }
    window.BPS.suggestWords = suggestWords, window.BPS.createDictionarySizeSelector = createDictionarySizeSelector, window.BPS.createSortControls = createSortControls, window.BPS.getCurrentDictionary = getCurrentDictionary, window.BPS.getCurrentSort = getCurrentSort;
  })();

  // src/ui/main.js
  window.BPS = window.BPS || {};
  (function() {
    "use strict";
    let styles = window.BPS.styles, applyStyles = window.BPS.applyStyles, setupDraggableResize = window.BPS.setupDraggableResize, createSettingsPanel = window.BPS.createSettingsPanel, createDictionarySizeSelector = window.BPS.createDictionarySizeSelector, createSortControls = window.BPS.createSortControls;
    function createUI() {
      let panel = document.createElement("div");
      panel.id = "bombPartyWordSuggesterPanel", applyStyles(panel, styles.panel), setupDraggableResize(panel);
      let content = document.createElement("div");
      content.id = "bombPartyWordSuggesterContent", panel.appendChild(content);
      let sizeSelector = createDictionarySizeSelector();
      content.appendChild(sizeSelector);
      let sortControls = createSortControls();
      content.appendChild(sortControls);
      let resultsDiv = document.createElement("div");
      resultsDiv.id = "bombPartyWordSuggesterResults", applyStyles(resultsDiv, styles.resultsDiv), resultsDiv.textContent = "(Waiting for syllable...)", content.appendChild(resultsDiv);
      let settingsPanel = createSettingsPanel(), settingsButton = document.createElement("button");
      settingsButton.textContent = "\u2699\uFE0F", applyStyles(settingsButton, styles.settingsButton), settingsButton.onclick = () => {
        settingsPanel.style.display = settingsPanel.style.display === "none" ? "block" : "none";
      }, settingsButton.onmousedown = (e) => e.stopPropagation(), panel.appendChild(settingsButton), document.body.appendChild(panel);
      let dictButtons = sizeSelector.querySelectorAll("button");
      dictButtons[1] && applyStyles(dictButtons[1], { ...styles.button, ...styles.activeButton });
      let sortButtons = sortControls.querySelectorAll("button");
      sortButtons[0] && (applyStyles(sortButtons[0], { ...styles.sortButton, ...styles.activeSortButton }), sortButtons[0].textContent = "Freq \u2193");
    }
    function initScript() {
      createUI(), window.BPS.setupSyllableObserver();
    }
    window.BPS.initScript = initScript;
  })();

  // src/ui/observer.js
  window.BPS = window.BPS || {};
  (function() {
    "use strict";
    let syllableObserver = null, suggestWords = window.BPS.suggestWords;
    function setupSyllableObserver() {
      if (syllableObserver) return;
      syllableObserver = new MutationObserver((mutations) => {
        for (let m of mutations)
          if (m.type === "childList" || m.type === "characterData") {
            let text = m.target.textContent.trim();
            text && suggestWords(text);
          }
      });
      function waitForSyllable() {
        let el = document.querySelector(".syllable");
        el ? (syllableObserver.observe(el, { childList: !0, characterData: !0, subtree: !0 }), el.textContent.trim() && suggestWords(el.textContent.trim())) : setTimeout(waitForSyllable, 1e3);
      }
      waitForSyllable();
    }
    window.BPS.setupSyllableObserver = setupSyllableObserver;
  })();

  // src/index.js
  (function() {
    "use strict";
    typeof window.BPS < "u" && window.BPS.loadAllDictionaries().then(() => {
      window.BPS.initScript();
    });
  })();
})();