Last active
January 14, 2026 17:09
-
-
Save Fronix/04a96cfa77c134a6612b936304dba6f6 to your computer and use it in GitHub Desktop.
numberpadToTekkenNotations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Dustloop Notation to Tekken (Preserve Colors, Toggle) | |
| // @namespace https://dustloop.com/ | |
| // @version 1.5.0 | |
| // @description Translate Dustloop numpad notation into Tekken shorthand inside code and colorful-text spans, preserving styling, with a toggle. | |
| // @match https://www.dustloop.com/* | |
| // @match https://dustloop.com/* | |
| // @run-at document-idle | |
| // @grant none | |
| // ==/UserScript== | |
| (() => { | |
| // Cleanup if re-injected | |
| try { window.__tekkenNotationToggleCleanup?.(); } catch (_) {} | |
| const OPTS = { | |
| // Motions to shorthand | |
| // 236 => QCF, 214 => QCB, 623 => DP | |
| motionMode: "shorthand", | |
| // Fallback formatting | |
| style: "slash", // "slash": d/f, d/b | "letters": df, db | |
| uppercaseDirs: false, | |
| motionSep: ",", | |
| dropNeutralPlus: true, | |
| }; | |
| const DIR_MAP_SLASH = { | |
| "1": "d/b", "2": "d", "3": "d/f", | |
| "4": "b", "5": "n", "6": "f", | |
| "7": "u/b", "8": "u", "9": "u/f", | |
| }; | |
| const DIR_MAP_LETTERS = { | |
| "1": "db", "2": "d", "3": "df", | |
| "4": "b", "5": "n", "6": "f", | |
| "7": "ub", "8": "u", "9": "uf", | |
| }; | |
| function mapDir(d) { | |
| const m = OPTS.style === "letters" ? DIR_MAP_LETTERS : DIR_MAP_SLASH; | |
| let out = m[d] || d; | |
| return OPTS.uppercaseDirs ? out.toUpperCase() : out; | |
| } | |
| function isButtonChar(ch) { | |
| return /[A-Za-z]/.test(ch); | |
| } | |
| function isRepeatOrCountContext(s, i) { | |
| const prev = s[i - 1] || ""; | |
| if (prev === "x" || prev === "X") return true; | |
| const nearby = s.slice(Math.max(0, i - 4), Math.min(s.length, i + 6)); | |
| if (/\(x\d/i.test(nearby) || /\(xN/i.test(nearby)) return true; | |
| return false; | |
| } | |
| function motionToShorthand(seq) { | |
| if (seq === "236") return "QCF"; | |
| if (seq === "214") return "QCB"; | |
| if (seq === "623") return "DP"; | |
| return null; | |
| } | |
| function normalizeNamedMotions(s) { | |
| // Normalize glued forms like QCFH -> QCF+H | |
| s = s.replace(/\bQCF(?=[A-Za-z])/gi, "QCF+"); | |
| s = s.replace(/\bQCB(?=[A-Za-z])/gi, "QCB+"); | |
| s = s.replace(/\bDP(?=[A-Za-z])/gi, "DP+"); | |
| return s; | |
| } | |
| function translateBracketedDirections(s) { | |
| // Only direction digit holds/releases | |
| s = s.replace(/\[([1-9])\]/g, (_, d) => `[${mapDir(d)}]`); | |
| s = s.replace(/\]([1-9])\[/g, (_, d) => `]${mapDir(d)}[`); | |
| return s; | |
| } | |
| function translateDirectionsAndMotions(s) { | |
| let out = ""; | |
| let i = 0; | |
| while (i < s.length) { | |
| const ch = s[i]; | |
| if (/[1-9]/.test(ch) && !isRepeatOrCountContext(s, i)) { | |
| let j = i; | |
| while (j < s.length && /[1-9]/.test(s[j]) && !isRepeatOrCountContext(s, j)) j++; | |
| const seq = s.slice(i, j); | |
| const next = s[j] || ""; | |
| const needsPlus = isButtonChar(next); | |
| if (seq.length > 1) { | |
| if (OPTS.motionMode === "shorthand") { | |
| const shorthand = motionToShorthand(seq); | |
| if (shorthand) { | |
| out += shorthand; | |
| if (needsPlus) out += "+"; | |
| } else { | |
| const mapped = seq.split("").map(mapDir); | |
| out += mapped.join(OPTS.motionSep); | |
| if (needsPlus) out += "+"; | |
| } | |
| } else { | |
| const mapped = seq.split("").map(mapDir); | |
| out += mapped.join(OPTS.motionSep); | |
| if (needsPlus) out += "+"; | |
| } | |
| } else { | |
| const single = mapDir(seq); | |
| const isNeutral = OPTS.uppercaseDirs ? single === "N" : single === "n"; | |
| if (!(OPTS.dropNeutralPlus && isNeutral && needsPlus)) { | |
| out += single; | |
| if (needsPlus) out += "+"; | |
| } | |
| } | |
| i = j; | |
| continue; | |
| } | |
| out += ch; | |
| i++; | |
| } | |
| return out; | |
| } | |
| function translateString(text) { | |
| let t = String(text); | |
| t = normalizeNamedMotions(t); | |
| t = translateBracketedDirections(t); | |
| t = translateDirectionsAndMotions(t); | |
| return t; | |
| } | |
| function containsNotation(text) { | |
| // Conservative "should translate" test | |
| return /([1-9]{2,}|[1-9]\s*[A-Za-z]|\[[1-9]\]|\][1-9]\[|\bQCF\b|\bQCB\b|\bDP\b|\bQCF(?=[A-Za-z])|\bQCB(?=[A-Za-z])|\bDP(?=[A-Za-z]))/i.test(text); | |
| } | |
| function shouldSkipElement(el) { | |
| const tag = (el.nodeName || "").toLowerCase(); | |
| if (["script", "style", "noscript", "textarea", "input"].includes(tag)) return true; | |
| if (el.closest && el.closest("script,style,noscript,textarea,input")) return true; | |
| return false; | |
| } | |
| function getColorfulClass(el) { | |
| if (!el || !el.classList) return null; | |
| for (const c of el.classList) { | |
| if (c.indexOf("colorful-text-") === 0) return c; | |
| } | |
| return null; | |
| } | |
| function mergeAdjacentColorSpans(container) { | |
| // Merge adjacent spans with same colorful-text-* class to avoid split digits like "2""3""6H" | |
| let node = container.firstChild; | |
| while (node) { | |
| if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "SPAN") { | |
| const cls = getColorfulClass(node); | |
| if (cls) { | |
| let next = node.nextSibling; | |
| while ( | |
| next && | |
| next.nodeType === Node.ELEMENT_NODE && | |
| next.tagName === "SPAN" && | |
| getColorfulClass(next) === cls | |
| ) { | |
| node.textContent += next.textContent; | |
| const rm = next; | |
| next = next.nextSibling; | |
| rm.remove(); | |
| } | |
| } | |
| } | |
| node = node.nextSibling; | |
| } | |
| } | |
| // Toggle support, store original HTML for modified elements | |
| const originalHtmlByEl = new Map(); | |
| let enabled = true; | |
| function translateOneElement(el) { | |
| if (shouldSkipElement(el)) return false; | |
| // Preserve exact original markup for toggling back | |
| if (!originalHtmlByEl.has(el)) originalHtmlByEl.set(el, el.innerHTML); | |
| // Merge adjacent colorful spans inside code blocks so motions are contiguous | |
| if (el.nodeName.toLowerCase() === "code") { | |
| mergeAdjacentColorSpans(el); | |
| } | |
| let changed = false; | |
| // If the element is itself a colorful span, translate its textContent directly | |
| const elColorClass = getColorfulClass(el); | |
| if (elColorClass) { | |
| const before = el.textContent; | |
| if (!containsNotation(before)) return false; | |
| const after = translateString(before); | |
| if (after !== before) { | |
| el.textContent = after; | |
| changed = true; | |
| } | |
| return changed; | |
| } | |
| // Otherwise, translate any colorful spans inside it (preserve coloring) | |
| const spans = el.querySelectorAll?.('span[class^="colorful-text-"]') || []; | |
| for (const sp of spans) { | |
| const before = sp.textContent; | |
| if (!containsNotation(before)) continue; | |
| const after = translateString(before); | |
| if (after !== before) { | |
| sp.textContent = after; | |
| changed = true; | |
| } | |
| } | |
| // Translate remaining text nodes within code blocks too (for commas, arrows, spaces, or raw notation) | |
| if (el.nodeName.toLowerCase() === "code") { | |
| const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, { | |
| acceptNode(n) { | |
| const v = n.nodeValue || ""; | |
| if (!v.trim()) return NodeFilter.FILTER_REJECT; | |
| if (!containsNotation(v)) return NodeFilter.FILTER_REJECT; | |
| return NodeFilter.FILTER_ACCEPT; | |
| } | |
| }); | |
| while (walker.nextNode()) { | |
| const tn = walker.currentNode; | |
| const before = tn.nodeValue; | |
| const after = translateString(before); | |
| if (after !== before) { | |
| tn.nodeValue = after; | |
| changed = true; | |
| } | |
| } | |
| } | |
| return changed; | |
| } | |
| function translateAllTargets() { | |
| // 1) All code blocks (including ComboText, and any other code) | |
| const codes = Array.from(document.querySelectorAll("code")); | |
| // 2) All colorful spans anywhere | |
| const colorfulSpans = Array.from(document.querySelectorAll('span[class^="colorful-text-"]')); | |
| // Deduplicate, some spans are inside code | |
| const targets = new Set([...codes, ...colorfulSpans]); | |
| let changedCount = 0; | |
| for (const el of targets) { | |
| const beforeText = el.textContent || ""; | |
| if (!beforeText.trim()) continue; | |
| if (!containsNotation(beforeText) && el.nodeName.toLowerCase() !== "code") continue; | |
| if (translateOneElement(el)) changedCount++; | |
| } | |
| return changedCount; | |
| } | |
| function restoreAll() { | |
| let restored = 0; | |
| for (const [el, html] of originalHtmlByEl.entries()) { | |
| if (!el || !el.isConnected) continue; | |
| el.innerHTML = html; | |
| restored++; | |
| } | |
| originalHtmlByEl.clear(); | |
| return restored; | |
| } | |
| const observer = new MutationObserver(() => { | |
| if (!enabled) return; | |
| translateAllTargets(); | |
| }); | |
| function makeButton() { | |
| const existing = document.getElementById("__tekken_notation_toggle_btn"); | |
| if (existing) existing.remove(); | |
| const btn = document.createElement("button"); | |
| btn.id = "__tekken_notation_toggle_btn"; | |
| btn.type = "button"; | |
| btn.textContent = "Tekken Notation: ON"; | |
| btn.style.position = "fixed"; | |
| btn.style.top = "10px"; | |
| btn.style.right = "10px"; | |
| btn.style.zIndex = "2147483647"; | |
| btn.style.padding = "6px 10px"; | |
| btn.style.fontSize = "12px"; | |
| btn.style.background = "#222"; | |
| btn.style.color = "#fff"; | |
| btn.style.border = "1px solid #555"; | |
| btn.style.borderRadius = "4px"; | |
| btn.style.cursor = "pointer"; | |
| btn.style.opacity = "0.9"; | |
| btn.addEventListener("click", () => { | |
| enabled = !enabled; | |
| if (enabled) { | |
| const n = translateAllTargets(); | |
| btn.textContent = "Tekken Notation: ON"; | |
| console.log(`Tekken Notation ON, updated ${n} elements.`); | |
| } else { | |
| const n = restoreAll(); | |
| btn.textContent = "Tekken Notation: OFF"; | |
| console.log(`Tekken Notation OFF, restored ${n} elements.`); | |
| } | |
| }); | |
| document.body.appendChild(btn); | |
| return btn; | |
| } | |
| function start() { | |
| if (!document.body) return; | |
| makeButton(); | |
| translateAllTargets(); | |
| observer.observe(document.body, { childList: true, subtree: true, characterData: true }); | |
| } | |
| start(); | |
| window.__tekkenNotationToggleCleanup = () => { | |
| try { observer.disconnect(); } catch (_) {} | |
| try { document.getElementById("__tekken_notation_toggle_btn")?.remove(); } catch (_) {} | |
| try { restoreAll(); } catch (_) {} | |
| }; | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment