Bomb Party Suggester

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
    });
  })();
})();