Skip to content

Instantly share code, notes, and snippets.

@BaliBalo
Created February 8, 2026 04:01
Show Gist options
  • Select an option

  • Save BaliBalo/9f491553a328e9ee33d0774d74115a76 to your computer and use it in GitHub Desktop.

Select an option

Save BaliBalo/9f491553a328e9ee33d0774d74115a76 to your computer and use it in GitHub Desktop.
Inkwell inputs - Adds alternative inputs handling for inkwell games (stars / fields)
// ==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