QoL improvements for CYOAs made in IntCyoaCreator
当前为
// ==UserScript==
// @name IntCyoaEnhancer
// @namespace https://agregen.gitlab.io/
// @version 0.2.4
// @description QoL improvements for CYOAs made in IntCyoaCreator
// @author agreg
// @license MIT
// @match https://*.neocities.org/*
// @icon https://intcyoacreator.onrender.com/favicon.ico?
// @run-at document-start
// @grant unsafeWindow
// @grant GM_registerMenuCommand
// ==/UserScript==
(function() {
'use strict';
// overriding AJAX sender (before the page starts loading) to detect project.json download done at init time
let init, enhance, _XHR = unsafeWindow.XMLHttpRequest;
unsafeWindow.XMLHttpRequest = class XHR extends _XHR {
constructor () {
super();
let _open = this.open;
this.open = (...args) => {
if ((`${args[0]}`.toUpperCase() === "GET") && (args[1] === "project.json")) {
init(() => this.addEventListener('loadend', enhance));
// displaying loading indicator if not present already (as a mod)
if (!document.getElementById('indicator')) {
let _indicator = document.createElement('div'), NBSP = '\xA0';
_indicator.style = `position: fixed; top: 0; left: 0; z-index: 1000`;
document.body.prepend(_indicator);
this.addEventListener('progress', e => {
_indicator.innerText = NBSP + "Loading data: " + (!e.total ? `${(e.loaded/1024**2).toFixed(1)} MB` :
`${(100 * e.loaded / e.total).toFixed(2)}%`);
});
this.addEventListener('loadend', () => {_indicator.innerText = ""});
}
}
return _open.apply(this, args);
};
}
};
init = (thunk=enhance) => {!init.done && (console.log("IntCyoaEnhancer!"), init.done = true, thunk())};
document.addEventListener('readystatechange', () =>
(document.readyState == 'complete') && ['activated', 'rows', 'pointTypes'].every(k => k in app.__vue__.$store.state.app) && init());
enhance = () => {
let _lazy = thunk => {let result, cached = false; return () => (cached ? result : cached = true, result = thunk())};
let _try = (thunk, fallback) => {try {return thunk()} catch (e) {console.error(e); return fallback}};
let _prompt = (message, value, thunk) => {let s = prompt(message, value); return (s != null) && thunk(s)};
let range = n => Array.from({length: n}, (_, i) => i);
let times = (n, f) => range(n).forEach(f);
// title & savestate are stored in URL hash
let _hash = _try(() => `["${decodeURIComponent( location.hash.slice(1) )}"]`); // it's a JSON array of 2 strings, without '["' & '"]' parts
let $save = [], [$title="", $saved=""] = _try(() => JSON.parse(_hash), []);
let $updateUrl = ({title=$title, save=$save}={}) => {location.hash = JSON.stringify([title, $saved=save.join(",")]).slice(2, -2)};
// app state accessors
let $store = () => app.__vue__.$store, $state = () => $store().state.app;
let $pointTypes = () => $state().pointTypes, $rows = () => $state().rows;
let $items = _lazy(() => [].concat( ...$rows().map(row => row.objects) ));
let $hiddenActive = _lazy(() => $items().filter(item => item.isSelectableMultiple || item.isImageUpload));
let $itemsMap = _lazy((m = new Map()) => ($items().forEach(item => m.set(item.id, item)), m)), $getItem = id => $itemsMap().get(id);
try {$store()} catch (e) {throw Error("[IntCyoaEnhancer] Can't access app state!", {cause: e})}
// logic taken from IntCyoaCreator as it appears to be hardwired into a UI component
let _selectedMulti = (item, num) => { // selecting a multi-value
let counter = 0, sign = Math.sign(num);
let _timesCmp = n => (sign < 0 ? item.numMultipleTimesMinus < n : item.numMultipleTimesPluss > n);
let _useMulti = () => _timesCmp(counter) && (item.multipleUseVariable = counter += sign, true);
let _addPoints = () => $pointTypes().filter(points => points.id == item.multipleScoreId).every(points =>
_timesCmp(points.startingSum) && (item.multipleUseVariable = points.startingSum += sign, true));
times(Math.abs(num), _ => {
if ((item.isMultipleUseVariable ? _useMulti() : _addPoints()))
item.scores.forEach(score => $pointTypes().filter(points => points.id == score.id)
.forEach(points => {points.startingSum -= sign * parseInt(score.value)}));
});
};
let _loadSave = save => { // applying a savestate
let _isHidden = s => s.includes("/ON#") || s.includes("/IMG#");
let tokens = save.split(','), activated = tokens.filter(s => !_isHidden(s)), hidden = tokens.filter(_isHidden);
let _split = (sep, item, token, fn, [id, arg]=token.split(sep, 2)) => {(id == item.id) && fn(arg)};
$store().commit({type: 'cleanActivated'}); // hopefully not broken…
$items().forEach(item => {
if (item.isSelectableMultiple)
hidden.forEach(token => _split("/ON#", item, token, num => _selectedMulti(item, parseInt(num))));
else if (item.isImageUpload)
hidden.forEach(token => _split("/IMG#", item, token, img => {item.image = img.replaceAll("/CHAR#", ",")}));
});
//$store().commit({type: 'addNewActivatedArray', newActivated: activated}); // not all versions have this :-(
let _activated = new Set(activated), _isActivated = id => _activated.has(id);
$state().activated = activated;
$rows().forEach(row => { // yes, four-level nested loop is how the app does everything
row.isEditModeOn = false;
delete row.allowedChoicesChange; // bugfix: cleanActivated is supposed to do this… but it doesn't
row.objects.filter(item => activated.includes(item.id)).forEach(item => {
item.isActive = true;
row.currentChoices += 1;
item.scores.forEach(score => $pointTypes().filter(points => points.id == score.id).forEach(points => {
if ((score.requireds.length <= 0) || $store().getters.checkRequireds(score)) {
score.isActive = true;
points.startingSum -= parseInt(score.value);
}
}));
});
});
};
// these are used for generating savestate
let _isActive = item => item.isActive || (item.isImageUpload && item.image) || (item.isSelectableMultiple && (item.multipleUseVariable !== 0));
let _activeId = item => (!_isActive(item) ? null : item.id + (item.isImageUpload ? `/IMG#${item.image.replaceAll(",", "/CHAR#")}` :
item.isSelectableMultiple ? `/ON#${item.multipleUseVariable}` : ""));
//let _activated = () => $items().map(_activeId).filter(Boolean); // this is how the app calculates it (selection order seems to be ignored)
let _hiddenActivated = () => $hiddenActive().filter(_isActive).map(item => item.id); // images and multi-vals are excluded from state
$store().watch(state => state.app.activated.filter(Boolean).concat( _hiddenActivated() ), // activated is formed incorrectly and may contain ""
ids => {$save = ids.map($getItem).map(_activeId), $updateUrl()}); // compared to the app """optimization""" this is blazing fast
let _bugfix = () => {
$rows().forEach(row => {delete row.allowedChoicesChange}); // This is a runtime variable, why is it exported?! It breaks reset!
};
// init && menu
_bugfix();
let _title = document.title;
$title && (document.title = $title);
$saved && confirm("Load state from URL?") && _loadSave($saved);
GM_registerMenuCommand("Change webpage title", () =>
_prompt("Change webpage title (empty to default)", $title||document.title, s => {document.title = ($title = s) || _title; $updateUrl()}));
GM_registerMenuCommand("Edit state", () =>
_prompt("Edit state (empty to reset)", $saved, _loadSave));
};
})();