Created
February 8, 2026 04:01
-
-
Save BaliBalo/9f491553a328e9ee33d0774d74115a76 to your computer and use it in GitHub Desktop.
Inkwell inputs - Adds alternative inputs handling for inkwell games (stars / fields)
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 Inkwell inputs | |
| // @namespace https://bali.balo.cool/ | |
| // @version 2026-01-17 | |
| // @description Adds alternative inputs handling for inkwell games | |
| // @author Bali Balo | |
| // @match https://inkwellgames.com/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=inkwellgames.com | |
| // @grant none | |
| // @run-at document-start | |
| // ==/UserScript== | |
| (function() { | |
| const LEFT_CLICK = 0; | |
| const MIDDLE_CLICK = 1; | |
| const RIGHT_CLICK = 2; | |
| const cells = { | |
| stars: { EMPTY: 0, CROSS: 1, STAR: 2 }, | |
| fields: { EMPTY: '.', BLUE: 'f1', GREEN: 'f2' }, | |
| }; | |
| const mappings = { | |
| stars: { | |
| default: { [MIDDLE_CLICK]: cells.stars.EMPTY, [RIGHT_CLICK]: cells.stars.STAR, toggle: true }, | |
| forced: { [LEFT_CLICK]: cells.stars.CROSS, [MIDDLE_CLICK]: cells.stars.EMPTY, [RIGHT_CLICK]: cells.stars.STAR }, | |
| }, | |
| fields: { | |
| default: { [MIDDLE_CLICK]: cells.fields.EMPTY, [RIGHT_CLICK]: cells.fields.BLUE }, | |
| forced: { [LEFT_CLICK]: cells.fields.GREEN, [MIDDLE_CLICK]: cells.fields.EMPTY, [RIGHT_CLICK]: cells.fields.BLUE }, | |
| }, | |
| }; | |
| const mappingNames = { | |
| forced: 'fixed (no cycling)', | |
| }; | |
| const getMappingName = (mapping) => mappingNames[mapping] || mapping; | |
| const toast = async (message, group) => { | |
| const div = document.createElement('div'); | |
| Object.assign(div.style, { | |
| position: 'fixed', | |
| top: '0', | |
| left: '50%', | |
| padding: '.25rem .75rem', | |
| border: '2px solid black', | |
| borderRadius: '50px', | |
| background: 'white', | |
| zIndex: 99, | |
| translate: '-50% -100%', | |
| }); | |
| div.innerHTML = message; | |
| if (group) { | |
| document.querySelectorAll(`div[data-toast-group="${group}"]`).forEach(el => el.stop?.()); | |
| div.dataset.toastGroup = group; | |
| } | |
| const earlyStop = Promise.withResolvers(); | |
| div.stop = earlyStop.resolve; | |
| document.body.append(div); | |
| await div.animate([{ translate: '-50% 24px' }], { duration: 300, fill: 'forwards', easing: 'ease' }).finished; | |
| await Promise.race([ | |
| new Promise(res => setTimeout(res, 1500)), | |
| earlyStop.promise, | |
| ]); | |
| await div.animate([{ translate: '-50% -100%' }], { duration: 300, fill: 'forwards', easing: 'ease' }).finished; | |
| div.remove(); | |
| }; | |
| const loadMapping = game => { | |
| const options = mappings[game]; | |
| try { return options[localStorage.getItem(`inkwell-inputs-${game}-mode`)] || options.default; } catch {} | |
| return options.default; | |
| }; | |
| window.MAPPINGS = { stars: loadMapping('stars'), fields: loadMapping('fields') }; | |
| const setMapping = (game, mapping) => { | |
| if (!mappings[game][mapping]) return; | |
| window.MAPPINGS[game] = mappings[game][mapping]; | |
| try { localStorage.setItem(`inkwell-inputs-${game}-mode`, mapping); } catch {} | |
| toast(`Input mode changed to <strong>${getMappingName(mapping)}</strong>`, 'mapping-update'); | |
| }; | |
| const cycleMapping = (game, direction = 1) => { | |
| const current = window.MAPPINGS[game]; | |
| const list = Object.keys(mappings[game]); | |
| const currentIndex = list.findIndex(key => mappings[game][key] === current); | |
| const nextMapping = currentIndex === -1 ? list[0] : list.at((currentIndex + direction) % list.length); | |
| setMapping(game, nextMapping); | |
| }; | |
| const fnFromString = str => new Function('return ' + str)(); | |
| const patches = [ | |
| // -- STARS -- | |
| { | |
| match: /cycleCell:\s*\(/, | |
| process: str => fnFromString(str.replace(/(cycleCell:\s*\([^)]+)(\).*?\.regions\.length)(\);)/, '$1,force$2,force$3')) | |
| }, | |
| { | |
| match: /\)\s*%\s*3;/, | |
| process: str => fnFromString(str.replace(/(function [^(]+\([^)]+)(\)[^}]+=)([^;,}]+%3;)/, '$1,force$2force??$3')) | |
| }, | |
| { | |
| match: /isDrawingXs/, | |
| process: str => fnFromString(str | |
| .replace(/(isDrawingXs:\s*!1)/g, '$1,isClearing:!1') | |
| .replace(/handleCellPointerStart:\s*\(([^)]+)\)\s*=>\s*{((?:[^{}]|{[^{}]*})*)}/, (_, args, body) => { | |
| const [row='t', col='r', board='n', evt='a'] = args.split(',').map(a => a.trim()); | |
| const ref = body.match(/(\w+)\.current/)?.[1] || 'l'; | |
| return `handleCellPointerStart:(${args})=>{let force=MAPPINGS.stars[${evt}.button],cur=${board}[${row}][${col}];${body.replace(/\)$/, ',MAPPINGS.stars.toggle&&force==cur?0:force)')};${ref}.current.isDrawingXs=force==1||(force==null&&0===cur);${ref}.current.isClearing=force==0}`; | |
| }) | |
| .replace(/([{;,])([^;,}]+\.current.isDrawingXs\s*&&(?:[^;,}(]|\([^);]*\))+)([;,}])/, (_, sep, body, end) => sep + body + ',' + body.replace('isDrawingXs', 'isClearing').replace(/\b0\s*===/, '0!==').replace(/\)$/, ',0)') + end) | |
| ) | |
| }, | |
| // -- FIELDS -- | |
| { | |
| match: /colorToPlace/, | |
| process: str => fnFromString(str | |
| .replace(/handleCellPointerStart:\s*\(([^)]+)\)\s*=>\s*{((?:[^{}]|{[^{}]*})*)}/, (_, args, body) => { | |
| const [, , , evt='c'] = args.split(',').map(a => a.trim()); | |
| return `handleCellPointerStart:(${args})=>{let force=MAPPINGS.fields[${evt}.button];${body.replace(/(colorToPlace\s*=\s*)(?!null)/, '$1force??')}}`; | |
| }) | |
| .replace(/([{;,])([^;,}]+\.current.isDrawingXs\s*&&(?:[^;,}(]|\([^);]*\))+)([;,}])/, (_, sep, body, end) => sep + body + ',' + body.replace('isDrawingXs', 'isClearing').replace(/\b0\s*===/, '0!==').replace(/\)$/, ',0)') + end) | |
| ) | |
| }, | |
| ]; | |
| const wp = (window.webpackChunk_N_E ||= []); | |
| let base = wp.push; | |
| const patcher = function(data) { | |
| for (let key in data?.[1] || {}) { | |
| const fn = data[1][key]; | |
| const fnStr = fn.toString(); | |
| for (let patch of patches) { | |
| if (patch.match.test(fnStr)) { | |
| // console.log('Patching function:', key); | |
| data[1][key] = patch.process(fnStr, fn, key) || fn; | |
| if (!patch.passthrough) break; | |
| } | |
| } | |
| } | |
| base.apply(this, arguments); | |
| }; | |
| patcher.bind = (thisArg, ...args) => Array.prototype.push.bind(wp, ...args); | |
| Object.defineProperty(wp, 'push', { | |
| set: v => { base = v; }, | |
| get: () => patcher, | |
| }); | |
| document.addEventListener('contextmenu', e => e.target?.matches('[data-game-grid], [data-game-grid] *, :has(> .pointer-events-none > [data-game-grid])') && e.preventDefault()); | |
| document.addEventListener('wheel', e => { | |
| if (Math.abs(e.deltaY) < 15) return; | |
| const grid = e.target?.closest('[data-game-grid]') || e.target.querySelector(':scope > .pointer-events-none > [data-game-grid]'); | |
| if (!grid) return; | |
| e.preventDefault(); | |
| const game = grid.dataset.gameGrid; | |
| if (game in mappings) { | |
| cycleMapping(game, Math.sign(e.deltaY)); | |
| } | |
| }, { passive: false }); | |
| document.addEventListener('keydown', e => { | |
| if (e.key !== '.') return; | |
| const game = document.querySelector('[data-game-grid]')?.dataset.gameGrid; | |
| if (game in mappings) { | |
| e.preventDefault(); | |
| cycleMapping(game); | |
| } | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment