// ==UserScript==
// @name Stimulation Splitter
// @namespace mikarific.com
// @description LiveSplit Auto-Split support for Stimulation Clicker!
// @icon https://raw.githubusercontent.com/Mikarific/StimulationSplitter/main/assets/userscript/icon.png
// @icon64 https://raw.githubusercontent.com/Mikarific/StimulationSplitter/main/assets/userscript/icon64.png
// @version 0.1.1
// @author Mikarific
// @match https://neal.fun/*
// @run-at document-start
// @noframes
// @inject-into browser
// @sandbox raw
// @connect github.com
// @supportURL https://discord.gg/Ka4ww68xnY
// @homepageURL https://discord.gg/Ka4ww68xnY
// @license MIT
// @grant GM.getValue
// @grant GM.registerMenuCommand
// @grant GM.setValue
// ==/UserScript==
(function () {
'use strict';
if (window.location.hostname === 'neal.fun') {
let finishedLoading = false;
window.history.pushState = new Proxy(history.pushState, {
apply: (target, thisArg, argsList) => {
if (argsList[2] !== undefined && finishedLoading) {
const newURL = new URL(argsList[2], document.baseURI);
const isStimulationClicker = newURL.hostname === 'neal.fun' && newURL.pathname === '/stimulation-clicker/';
if (isStimulationClicker) location.replace(newURL);
}
return Reflect.apply(target, thisArg, argsList);
}
});
window.history.replaceState = new Proxy(history.replaceState, {
apply: (target, thisArg, argsList) => {
if (argsList[2] !== undefined && finishedLoading) {
const newURL = new URL(argsList[2], document.baseURI);
const isStimulationClicker = newURL.hostname === 'neal.fun' && newURL.pathname === '/stimulation-clicker/';
if (isStimulationClicker) location.replace(newURL);
}
return Reflect.apply(target, thisArg, argsList);
}
});
window.addEventListener('popstate', () => {
if (finishedLoading) {
const newURL = new URL(window.location.href);
const isStimulationClicker = newURL.hostname === 'neal.fun' && newURL.pathname === '/stimulation-clicker/';
if (isStimulationClicker) location.replace(newURL);
}
});
if (document.readyState === 'interactive') {
finishedLoading = true;
} else {
window.addEventListener('DOMContentLoaded', () => {
finishedLoading = true;
}, {
once: true
});
}
}
function domPromise(elem) {
return new Promise(resolve => {
if (window.location.hostname === 'neal.fun' && window.location.pathname === '/stimulation-clicker/') {
if (document.readyState === 'interactive') {
resolve(elem());
} else {
window.addEventListener('DOMContentLoaded', () => {
resolve(elem());
}, {
once: true
});
}
} else {
resolve(null);
}
});
}
const container = domPromise(() => document.querySelector('.container'));
const dom = {
container
};
function getVueState(container, resolve) {
if (container.__vue__ !== undefined) {
const vueState = container.__vue__.stimulation === undefined ? container.__vue__.$children.find(child => child.stimulation !== undefined) : container.__vue__;
resolve(vueState);
} else {
Object.defineProperty(container, '__vue__', {
set(vueState) {
if (vueState !== null) {
resolve(vueState);
Object.defineProperty(container, '__vue__', {
value: vueState,
writable: true,
configurable: true,
enumerable: true
});
}
},
configurable: true,
enumerable: true
});
}
}
const state = new Promise(resolve => {
if (window.location.hostname === 'neal.fun' && window.location.pathname === '/stimulation-clicker/') {
if (document.readyState === 'complete') {
dom.container.then(async container => getVueState(await container, resolve));
} else {
window.addEventListener('load', () => {
dom.container.then(async container => getVueState(await container, resolve));
}, {
once: true
});
}
} else {
resolve(null);
}
});
async function startLiveSplit() {
const livesplitServer = new WebSocket('ws://127.0.0.1:16834/livesplit');
livesplitServer.onopen = () => {
livesplitServer.send('reset');
};
function getSplitIndex() {
return new Promise(resolve => {
livesplitServer.onmessage = event => {
livesplitServer.onmessage = null;
resolve(parseInt(event.data));
};
livesplitServer.send('getsplitindex');
});
}
async function split(splitTo) {
let splitIndex = await getSplitIndex();
for (let i = Math.max(splitIndex, 0); i < splitTo; i++) {
livesplitServer.send('skipsplit');
splitIndex++;
}
if (splitIndex === splitTo) livesplitServer.send('split');
}
const vueState = await state;
// When stimulation is first shown, start the timer
const {
set: showStimulationSetter
} = Object.getOwnPropertyDescriptor(vueState, 'showStimulation');
Object.defineProperty(vueState, 'showStimulation', {
set(showStimulation) {
if (!vueState.showStimulation && showStimulation) livesplitServer.send('starttimer');
return showStimulationSetter.call(this, showStimulation);
},
configurable: true,
enumerable: true
});
// When Hydraulic Press is purchased, split to index 0
const {
set: showHydraulicPressSetter
} = Object.getOwnPropertyDescriptor(vueState, 'showHydraulicPress');
Object.defineProperty(vueState, 'showHydraulicPress', {
set(showHydraulicPress) {
if (!vueState.showHydraulicPress && showHydraulicPress) split(0);
return showHydraulicPressSetter.call(this, showHydraulicPress);
},
configurable: true,
enumerable: true
});
// When Levels is purchased, split to index 1
const {
set: showLevelsSetter
} = Object.getOwnPropertyDescriptor(vueState, 'showLevels');
Object.defineProperty(vueState, 'showLevels', {
set(showLevels) {
if (!vueState.showLevels && showLevels) split(1);
return showLevelsSetter.call(this, showLevels);
},
configurable: true,
enumerable: true
});
// When Stock Market is purchased, split to index 2
const {
set: showStockMarketSetter
} = Object.getOwnPropertyDescriptor(vueState, 'showStockMarket');
Object.defineProperty(vueState, 'showStockMarket', {
set(showStockMarket) {
if (!vueState.showStockMarket && showStockMarket) split(2);
return showStockMarketSetter.call(this, showStockMarket);
},
configurable: true,
enumerable: true
});
// When Email is purchased, split to index 3
const {
set: inboxUnlockedSetter
} = Object.getOwnPropertyDescriptor(vueState, 'inboxUnlocked');
Object.defineProperty(vueState, 'inboxUnlocked', {
set(inboxUnlocked) {
if (!vueState.inboxUnlocked && inboxUnlocked) split(3);
return inboxUnlockedSetter.call(this, inboxUnlocked);
},
configurable: true,
enumerable: true
});
// When Crypto is purchased, split to index 4
const {
set: cryptoUnlockedSetter
} = Object.getOwnPropertyDescriptor(vueState, 'cryptoUnlocked');
Object.defineProperty(vueState, 'cryptoUnlocked', {
set(cryptoUnlocked) {
if (!vueState.cryptoUnlocked && cryptoUnlocked) split(4);
return cryptoUnlockedSetter.call(this, cryptoUnlocked);
},
configurable: true,
enumerable: true
});
// When Leverage is purchased, split to index 5
const {
set: stockLeverageSetter
} = Object.getOwnPropertyDescriptor(vueState, 'stockLeverage');
Object.defineProperty(vueState, 'stockLeverage', {
set(stockLeverage) {
if (vueState.stockLeverage === 1 && stockLeverage === 2) split(5);
return stockLeverageSetter.call(this, stockLeverage);
},
configurable: true,
enumerable: true
});
// When Subway Surfers Wormhole is purchased, split to index 6
vueState.startWormhole = new Proxy(vueState.startWormhole, {
apply: (target, thisArg, argsList) => {
split(6);
return Reflect.apply(target, thisArg, argsList);
}
});
// When Go to the Ocean is purchased, split to index 7
vueState.endGame = new Proxy(vueState.endGame, {
apply: (target, thisArg, argsList) => {
split(7);
return Reflect.apply(target, thisArg, argsList);
}
});
}
if (window.location.hostname === 'neal.fun' && window.location.pathname === '/stimulation-clicker/') {
if (document.readyState === 'complete') {
startLiveSplit();
} else {
window.addEventListener('load', startLiveSplit, {
once: true
});
}
}
async function patchDVDs() {
if (await GM.getValue('dvdStandardization', true)) {
const vueState = await state;
const bgState = vueState.$refs.bg;
const renderer = bgState.$refs.renderer;
renderer.style.width = '1920px';
renderer.style.height = '1080px';
renderer.style.position = 'fixed';
renderer.style.top = '50%';
renderer.style.left = '50%';
renderer.style.transform = 'translate(-50%, -50%)';
// We don't have direct access to updateDVDs as it's not part of vue data,
// but we can set the size of the window to 1920x1080 before the dvd hits
// calculation by setting them before the start of bgAnimationLoop, and
// resetting them after the bgAnimationLoop function has executed.
vueState.bgAnimationLoop = new Proxy(vueState.bgAnimationLoop, {
apply: (target, thisArg, argsList) => {
const realWidth = window.innerWidth;
const realHeight = window.innerHeight;
window.innerWidth = 1920;
window.innerHeight = 1080;
const returnValue = Reflect.apply(target, thisArg, argsList);
window.innerWidth = realWidth;
window.innerHeight = realHeight;
return returnValue;
}
});
// This is purely for the visuals, actually renders the DVDs at the size of the canvas (1920x1080)
bgState.resize();
}
}
if (window.location.hostname === 'neal.fun' && window.location.pathname === '/stimulation-clicker/') {
if (document.readyState === 'complete') {
patchDVDs();
} else {
window.addEventListener('load', patchDVDs, {
once: true
});
}
}
GM.registerMenuCommand('Toggle DVD Standardization', async () => {
const dvdStandardization = await GM.getValue('dvdStandardization', true);
GM.setValue('dvdStandardization', !dvdStandardization);
if (dvdStandardization) alert('DVD Standardization has been turned OFF');
if (!dvdStandardization) alert('DVD Standardization has been turned ON');
location.reload();
});
})();