IntCyoaEnhancer

QoL improvements for CYOAs made in IntCyoaCreator

目前为 2022-02-20 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name IntCyoaEnhancer
  3. // @namespace https://agregen.gitlab.io/
  4. // @version 0.5
  5. // @description QoL improvements for CYOAs made in IntCyoaCreator
  6. // @author agreg
  7. // @license MIT
  8. // @match https://*.neocities.org/*
  9. // @icon https://intcyoacreator.onrender.com/favicon.ico?
  10. // @run-at document-start
  11. // @require https://unpkg.com/mreframe/dist/mreframe.js
  12. // @require https://unpkg.com/lz-string/libs/lz-string.js
  13. // @grant unsafeWindow
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_addStyle
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // overriding AJAX sender (before the page starts loading) to detect project.json download done at init time
  22. let init, enhance, _XHR = unsafeWindow.XMLHttpRequest;
  23. unsafeWindow.XMLHttpRequest = class XHR extends _XHR {
  24. constructor () {
  25. super();
  26. let _open = this.open;
  27. this.open = (...args) => {
  28. if ((`${args[0]}`.toUpperCase() === "GET") && (`${args[1]}`.match(/^project.json$|^js\/app\.\w*\.js$/))) {
  29. init(() => this.addEventListener('loadend', () => setTimeout(enhance)));
  30. // displaying loading indicator if not present already (as a mod)
  31. if (!document.getElementById('indicator')) {
  32. let _indicator = document.createElement('div'), NBSP = '\xA0';
  33. _indicator.style = `position: fixed; top: 0; left: 0; z-index: 1000`;
  34. _indicator.title = args[1];
  35. document.body.prepend(_indicator);
  36. this.addEventListener('progress', e => {
  37. _indicator.innerText = NBSP + "Loading data: " + (!e.total ? `${(e.loaded/1024**2).toFixed(1)} MB` :
  38. `${(100 * e.loaded / e.total).toFixed(2)}%`);
  39. });
  40. this.addEventListener('loadend', () => {_indicator.innerText = ""});
  41. }
  42. }
  43. return _open.apply(this, args);
  44. };
  45. }
  46. };
  47.  
  48. init = (thunk=enhance) => {!init.done && (console.log("IntCyoaEnhancer!"), init.done = true, thunk())};
  49. document.addEventListener('readystatechange', () =>
  50. (document.readyState == 'complete') && ['activated', 'rows', 'pointTypes'].every(k => k in app.__vue__.$store.state.app) && init());
  51.  
  52. enhance = () => {
  53. let {isArray} = Array, isJson = x => (typeof x === 'string') && x.trim().match(/^{.*}$/); // minimal check
  54. let range = n => Array.from({length: n}, (_, i) => i);
  55. let times = (n, f) => range(n).forEach(f);
  56. let _lazy = thunk => {let result, cached = false; return () => (cached ? result : cached = true, result = thunk())};
  57. let _try = (thunk, fallback, quiet=false) => {try {return thunk()} catch (e) {quiet||console.error(e); return fallback}};
  58. let _prompt = (message, value, thunk) => {let s = prompt(message, (typeof value == 'string' ? value : JSON.stringify(value)));
  59. return (s == null ? s : thunk(s))};
  60. let _node = (tag, attr, ...children) => {
  61. let node = Object.assign(document.createElement(tag), attr);
  62. children.forEach(child => {node.append(isArray(child) ? _node(...child) : document.createTextNode(`${child||""}`))});
  63. return node;
  64. };
  65. let _debounce = (thunk, msec) => function $debounce () {
  66. clearTimeout($debounce.delay);
  67. $debounce.delay = setTimeout(() => {
  68. $debounce.delay = null;
  69. thunk();
  70. }, msec);
  71. };
  72.  
  73. // title & savestate are stored in URL hash
  74. let _hash = _try(() => `["${decodeURIComponent( location.hash.slice(1) )}"]`); // it's a JSON array of 2 strings, without '["' & '"]' parts
  75. let $save = [], [$title="", $saved="", $snapshot=""] = _try(() => JSON.parse(_hash), []);
  76. let _encode = o => LZString.compressToBase64(isJson(o) ? o : JSON.stringify(o)),
  77. _decode = s => (isJson(s) ? JSON.parse(s) : JSON.parse(LZString.decompressFromBase64(s) || (_ => {throw Error("Invalid input")})()));
  78. let $updateUrl = ({title=$title, save=$save, snapshot=$snapshot}={}) =>
  79. {location.hash = JSON.stringify([title, ...(!snapshot ? [$saved=save.join(",")] : ["", snapshot])]).slice(2, -2)};
  80. // app state accessors
  81. let $store = () => app.__vue__.$store, $state = () => $store().state.app;
  82. let $pointTypes = () => $state().pointTypes, $rows = () => $state().rows;
  83. let $items = _lazy(() => [].concat( ...$rows().map(row => row.objects) ));
  84. let $hiddenActive = _lazy(() => $items().filter(item => item.isSelectableMultiple || item.isImageUpload));
  85. let $itemsMap = _lazy((m = new Map()) => ($items().forEach(item => m.set(item.id, item)), m)), $getItem = id => $itemsMap().get(id);
  86. let _fatKeys = x => ['backgroundImage', 'rowBackgroundImage'].concat(x.isImageUpload ? [] : ['image']);
  87. let _slim = x => x && (typeof x !== 'object' ? x : isArray(x) ? x.map(_slim) :
  88. Object.assign({}, x, ..._fatKeys(x).map(k => ({[k]: void 0})),
  89. ...Object.keys(x).filter(k => typeof x[k] === 'object').map(k => x[k] && ({[k]: _slim(x[k])}))));
  90. let $slimStateCopy = (state=$state()) => $clone( _slim(state) );
  91. try {$store()} catch (e) {throw Error("[IntCyoaEnhancer] Can't access app state!", {cause: e})}
  92.  
  93. // logic taken from IntCyoaCreator as it appears to be hardwired into a UI component
  94. let _selectedMulti = (item, num) => { // selecting a multi-value
  95. let counter = 0, sign = Math.sign(num);
  96. let _timesCmp = n => (sign < 0 ? item.numMultipleTimesMinus < n : item.numMultipleTimesPluss > n);
  97. let _useMulti = () => _timesCmp(counter) && (item.multipleUseVariable = counter += sign, true);
  98. let _addPoints = () => $pointTypes().filter(points => points.id == item.multipleScoreId).every(points =>
  99. _timesCmp(points.startingSum) && (item.multipleUseVariable = points.startingSum += sign, true));
  100. times(Math.abs(num), _ => {
  101. if ((item.isMultipleUseVariable ? _useMulti() : _addPoints()))
  102. item.scores.forEach(score => $pointTypes().filter(points => points.id == score.id)
  103. .forEach(points => {points.startingSum -= sign * parseInt(score.value)}));
  104. });
  105. };
  106. let _loadSave = save => { // applying a savestate
  107. let _isHidden = s => s.includes("/ON#") || s.includes("/IMG#");
  108. let tokens = save.split(','), activated = tokens.filter(s => s && !_isHidden(s)), hidden = tokens.filter(_isHidden);
  109. let _split = (sep, item, token, fn, [id, arg]=token.split(sep, 2)) => {(id == item.id) && fn(arg)};
  110. $store().commit({type: 'cleanActivated'}); // hopefully not broken…
  111. $items().forEach(item => {
  112. if (item.isSelectableMultiple)
  113. hidden.forEach(token => _split("/ON#", item, token, num => _selectedMulti(item, parseInt(num))));
  114. else if (item.isImageUpload)
  115. hidden.forEach(token => _split("/IMG#", item, token, img => {item.image = img.replaceAll("/CHAR#", ",")}));
  116. });
  117. //$store().commit({type: 'addNewActivatedArray', newActivated: activated}); // not all versions have this :-(
  118. let _activated = new Set(activated), _isActivated = id => _activated.has(id);
  119. $state().activated = activated;
  120. $rows().forEach(row => { // yes, four-level nested loop is how the app does everything
  121. row.isEditModeOn = false;
  122. delete row.allowedChoicesChange; // bugfix: cleanActivated is supposed to do this… but it doesn't
  123. row.objects.filter(item => _isActivated(item.id)).forEach(item => {
  124. item.isActive = true;
  125. row.currentChoices += 1;
  126. item.scores.forEach(score => $pointTypes().filter(points => points.id == score.id).forEach(points => {
  127. if (!score.requireds || (score.requireds.length <= 0) || $store().getters.checkRequireds(score)) {
  128. score.isActive = true;
  129. points.startingSum -= parseInt(score.value);
  130. }
  131. }));
  132. });
  133. });
  134. };
  135. // these are used for generating savestate
  136. let _isActive = item => item && (item.isActive || (item.isImageUpload && item.image) || (item.isSelectableMultiple && (item.multipleUseVariable !== 0)));
  137. let _activeId = item => (!_isActive(item) ? null : item.id + (item.isImageUpload ? `/IMG#${item.image.replaceAll(",", "/CHAR#")}` :
  138. item.isSelectableMultiple ? `/ON#${item.multipleUseVariable}` : ""));
  139. //let _activated = () => $items().map(_activeId).filter(Boolean); // this is how the app calculates it (selection order seems to be ignored)
  140.  
  141. let $hiddenActivated = () => $hiddenActive().filter(_isActive).map(item => item.id); // images and multi-vals are excluded from state
  142. $store().watch(state => state.app.activated.filter(Boolean).concat( $hiddenActivated() ), // activated is formed incorrectly and may contain ""
  143. ids => {$save = ids.map($getItem).filter(Boolean).map(_activeId), $updateUrl()}); // compared to the app """optimization""" this is blazing fast
  144.  
  145. let diff = initial => (current=$slimStateCopy(), cheat=$cheat.data) => {
  146. let _cheat = (function $slim (o) {
  147. if (!o || isArray(o) || (typeof o != 'object')) return o;
  148. let kvs = Object.entries(o).filter(([k, v]) => $slim(v));
  149. return (kvs.length == 0 ? void 0 : Object.fromEntries(kvs));
  150. })(cheat);
  151. return (function $diff (a, b/*, ...path*/) {
  152. if ((typeof a !== typeof b) || (isArray(a) !== isArray(b)) || (isArray(a) && (a.length !== b.length)))
  153. return b;
  154. else if (a && b && (typeof a === 'object')) {
  155. let res = Object.entries(b).map(([k, v]) => [k, $diff(a[k], v/*, ...path, k*/)]).filter(([k, v]) => v !== void 0);
  156. if (res.length > 0) return Object.fromEntries(res);
  157. } else if (a === a ? a !== b : b === b)
  158. return b;
  159. })(initial, {_cheat, ...current}) || {};
  160. };
  161. let restoreSnapshot = initial => (snapshot=$snapshot) => _try(() => {
  162. let {reFrame: rf, util: {getIn, assoc, isArray, isDict, keys}} = require('mreframe');
  163. let {_cheat, ..._state} = (typeof snapshot !== 'string' ? snapshot : _decode(snapshot||"{}"));
  164. let newState = (function $deepMerge (a, b) {
  165. return (!isDict(b) ? a : keys(b).reduce((o, k) => ((o[k] = (!isDict(b[k]) ? b[k] : $deepMerge(a[k], b[k]))), o), a));
  166. })($clone(initial), _state);
  167. (function $updState (a, x/*, ...path*/) {
  168. a && (typeof a == 'object') && keys(a).forEach(k => {
  169. isArray(a[k]) && (x[k] = (!isArray(x[k]) ? a[k] : x[k].slice(0, a[k].length).concat( a[k].slice(x[k].length) )));
  170. (!(k in x) || (typeof a[k] != 'object') ? x[k] = a[k] : $updState(a[k], x[k]/*, ...path, k*/));
  171. });
  172. isDict(x) && keys(x).filter(k => !(k in a) && !_fatKeys(x).includes(k)).forEach(k => {delete x[k]});
  173. })(newState, $state());
  174. (_cheat || $cheat.toggle) && ($cheat.toggle || $cheat(), rf.disp(['init-db', $cheat.data = _cheat]));
  175. $snapshot = _encode({_cheat, ..._state});
  176. $updateUrl();
  177. return true;
  178. }) || alert("State load failed. (Possible reason: invalid state snapshot.)");
  179.  
  180. // debug functions for console
  181. let $activated = () => $state().activated, $clone = x => JSON.parse(JSON.stringify(x));
  182. let $rowsActive = () => $rows().map(row => [row, row.objects.filter(_isActive)]).filter(([_, items]) => items.length > 0);
  183. let $dbg = {$store, $state, $pointTypes, $rows, $items, $getItem, $activated, $hiddenActivated, $rowsActive, $clone, $slimStateCopy};
  184. Object.assign(unsafeWindow, {$dbg}, $dbg);
  185.  
  186. let _bugfix = () => {
  187. $rows().forEach(row => {delete row.allowedChoicesChange}); // This is a runtime variable, why is it exported?! It breaks reset!
  188. };
  189.  
  190. // init && menu
  191. _bugfix();
  192. let _title = document.title, _initial = $slimStateCopy(), _restore = restoreSnapshot(_initial), _diff = diff(_initial);
  193. Object.assign(unsafeWindow, {$initial: JSON.stringify(_initial).length, $diff: _diff, $encode: _encode, $decode: _decode});
  194. $title && (document.title = $title);
  195. ($saved||$snapshot) && confirm("Load state from URL?") && (!$snapshot ? _loadSave($saved) : _restore($snapshot));
  196. let _syncSnapshot = _debounce(() => {$snapshot = _encode(_diff()), $updateUrl()}, 1000);
  197. let $watch = (snapshot=($snapshot ? "" : _encode( _diff() ))) => {
  198. document.body.classList[snapshot ? 'add' : 'remove']('-FULL-SCAN');
  199. $snapshot = snapshot;
  200. $watch.stop = $watch.stop && ($watch.stop(), null);
  201. snapshot && ($watch.stop = $store().watch(x => x, _syncSnapshot, {deep: true}));
  202. $updateUrl();
  203. };
  204. $snapshot && $watch($snapshot);
  205. GM_registerMenuCommand("Change webpage title", () =>
  206. _prompt("Change webpage title (empty to default)", $title||document.title, s => {document.title = ($title = s) || _title; $updateUrl()}));
  207. GM_registerMenuCommand("Edit state", () =>
  208. _prompt("Edit state (empty to reset)", ...(!$snapshot ? [$saved, _loadSave] : [_decode($snapshot), _restore])));
  209. GM_registerMenuCommand("Toggle full scan mode", $watch);
  210. GM_registerMenuCommand("Download project data", () => Object.assign(document.createElement('a'), {
  211. download: "project.json", href: `data:application/json,${encodeURIComponent(JSON.stringify($state()) + "\n")}`,
  212. }).click());
  213. ($state().backpack.length == 0) && GM_registerMenuCommand("Enable backpack", function $addBackpack(prefix) {
  214. _prompt([prefix, "How many choices should be displayed in a row? (1-4)"].filter(Boolean).join("\n"), "3", num =>
  215. (!["1", "2", "3", "4"].includes(num) ? setTimeout(() => $addBackpack(`Sorry, ${JSON.stringify(num)} is not a valid column number.`)) :
  216. ($state().backpack = [{title: "Selected choices", titleText: "", template: "1", isInfoRow: true, isResultRow: true,
  217. objectWidth: `col-md-${{1: 12, 2: 6, 3: 4, 4: 3}[num]}`}])));
  218. });
  219.  
  220. let $overview = () => {
  221. if ($overview.toggle)
  222. $overview.toggle();
  223. else {
  224. const _ID = 'LIST', ID = '#'+_ID, _scroll = (s, bg='#2B2F35', thumb='grey', wk='::-webkit-scrollbar') =>
  225. `${s} {scrollbar-width:thin; scrollbar-color:${thumb} ${bg}} ${s}${wk} {width:6px; height:6px; background:${bg}} ${s}${wk}-thumb {background:${thumb}}`;
  226. GM_addStyle(`${ID} {position:fixed; top:0; left:0; height:100%; width:100%; background:#0008; z-index:1001}
  227. ${ID} img {position:fixed; top:0; max-height:40%; object-fit:contain; background:#000B}
  228. ${ID} .-nav .-row-name {cursor:pointer; padding:2px 1ex} ${ID} .-nav .-row-name:hover {background:var(--gray)}
  229. ${ID} .-item-name {font-weight:bold} ${ID} .-dialog :is(.-row-name, .-item):hover {cursor:help; text-shadow:0 0 10px}
  230. ${ID} .-roll :is(input, button) {width:2.5em; color:black; background:var(--light)}
  231. ${ID} .-roll button {border-radius:2ex} ${ID} input[type=number] {text-align:right} ${ID} input:invalid {background:var(--red)}` +
  232. [[" .-roll", "0", "20%", "#0008"], [" .-dialog", "20%", "60%", "var(--dark)"], [" .-nav", "80%", "20%", "#0008"]].map(([k, left, width, bg]) =>
  233. `${ID}${k} {position:fixed; top:40%; left:${left}; height:calc(60% - 56px); width:${width}; color:var(--light); background:${bg};
  234. padding:1em; overflow-y:auto} ${_scroll(ID+k)}`).join("\n"));
  235. document.body.append($overview.overlay = _node('div', {id: _ID, onclick: $overview}));
  236. $overview.overlay.append($overview.image = _node('img'));
  237. $overview.overlay.append($overview.activated = _node('div', {className: '-dialog', title: "Activated items", onclick: e => e.stopPropagation()}));
  238. $overview.overlay.append($overview.nav = _node('div', {className: '-nav', title: "Navigation (visible rows)", onclick: e => e.stopPropagation()}));
  239. $overview.overlay.append($overview.roll = _node('div', {className: '-roll', title: "Dice roll", onclick: e => e.stopPropagation()}));
  240. document.addEventListener('keydown', e => (e.key == 'Escape') && $overview.toggle(true));
  241. let _points = Object.fromEntries( $pointTypes().map(points => [points.id, `[${points.id}] `+ (points.beforeText || `(${points.name})`)]) );
  242. let _ptReqOp = {1: ">", 2: "≥", 3: "=", 4: "≤", 5: "<"}, _ptReqCmpOp = {1: ">", 2: "=", 3: "≥"};
  243. let _req = score => x => (x.required ? "" : "NOT!") + ({id: x.reqId||"?", points: `${x.reqId||"?"} ${_ptReqOp[x.operator]} ${x.reqPoints}`,
  244. pointCompare: `${x.reqId||"?"} ${_ptReqCmpOp[x.operator]} ${x.reqId1||"?"}`})[x.type] || "???";
  245. let _cost = score => " " + (_points[score.id] || `"${score.beforeText}"`) + (score.value > 0 ? " " : " +") + (-parseInt(score.value||0)) +
  246. ((score.requireds||[]).length == 0 ? "" : "\t{" + score.requireds.map(_req(score)).join(" & ") + "}");
  247. let _showImg = ({image}) => () => ($overview.image.src = image) && ($overview.image.style.display = '');
  248. let _hideImg = () => {[$overview.image.src, $overview.image.style.display] = ["", 'none']};
  249. let _rowAttrs = row => ({className: '-row-name', title: `[${row.id}]\n\n${row.titleText}`.trim(), onmouseenter: _showImg(row), onmouseleave: _hideImg});
  250. let _nav = e => () => {$overview.toggle(true); e.scrollIntoView({block: 'start'})};
  251. let _dice = [1, 6, 0], _roll = (n, m, k) => (_dice = [n, m, k, range(n).reduce(res => res + Math.floor(1 + m*Math.random()), k)], _dice[3]);
  252. let _setDice = idx => function () {this.value = parseInt(this.value)||_dice[idx]; _dice.splice(idx, 1, this.valueAsNumber)};
  253. $overview.toggle = (visible = !$overview.overlay.style.display) => {
  254. if (!visible) {
  255. $overview.roll.innerHTML = "<h3>Roll</h3>";
  256. $overview.roll.append( _node('div', {},
  257. ['p', {}, ['input', {type: 'number', title: "N", min: 1, value: _dice[0], onchange: _setDice(0)}], " d ",
  258. ['input', {type: 'number', title: "M", min: 2, value: _dice[1], onchange: _setDice(1)}], " + ",
  259. ['input', {type: 'number', title: "K", value: _dice[2], onchange: _setDice(2)}], " = ",
  260. ['button', {title: "ROLL", onclick () {this.innerText = _roll(..._dice)}}, `${_dice.length < 4 ? "(roll)" : _dice[3]}`]],
  261. "(NdM+K means rolling an M-sided die N times and adding K to the total)") );
  262. $overview.nav.innerHTML = "<h3>Navigation</h3>";
  263. [...document.querySelectorAll(["* > .row", "*"].map(s => `.v-application--wrap > ${s} > :not(.v-bottom-navigation) > :not(.col)`).join(", "))]
  264. .filter(e => !e.style.display).map(e => [e, e.__vue__._props.row])
  265. .forEach(([e, row]) => {$overview.nav.append( _node('div', {..._rowAttrs(row), onclick: _nav(e)}, row.title.trim() || ['i', {}, row.id]) )});
  266. $overview.activated.innerHTML = "<h3>Activated</h3>";
  267. $rowsActive().forEach(([row, items]) => {
  268. $overview.activated.append( _node('p', {className: '-row'},
  269. ['span', _rowAttrs(row), row.title.trim() || ['i', {}, row.id]],
  270. ": ",
  271. ...[].concat(...items.map(item => [
  272. ", ",
  273. ['span', {className: '-item', title: [`[${item.id}]`, item.text, item.scores.map(_cost).join("\n")].filter(Boolean).join("\n\n").trim(),
  274. onmouseenter: _showImg(item), onmouseleave: _hideImg},
  275. ['span', {className: '-item-name'}, item.title.trim() || ['i', {}, item.id]],
  276. !item.isActive && (item.isSelectableMultiple ? ` ${item.multipleUseVariable}}` : " {Image}")],
  277. ])).slice(1)));
  278. });
  279. }
  280. $overview.overlay.style.display = (visible ? 'none' : '');
  281. }
  282. $overview.toggle(false);
  283. }
  284. };
  285. GM_registerMenuCommand("Overview", $overview);
  286. GM_addStyle(`#LIST-TOGGLE {position:fixed; right:3px; bottom:3px; z-index:1001; color:var(--light); background:var(--gray);
  287. padding:1ex; width:auto; border-radius:1em}
  288. .-FULL-SCAN #LIST-TOGGLE {color:var(--gray); background:var(--light)}`);
  289. document.body.append( _node('button', {id: 'LIST-TOGGLE', className: "v-icon mdi mdi-table-of-contents", title: "Overview/dice roll", onclick: $overview}) );
  290.  
  291. function $cheat() {
  292. if (!$cheat.toggle) {
  293. const {reFrame: rf, reagent: r, util: {getIn, update, assocIn, merge, entries}} = require('mreframe');
  294. let updateIn = (o, path, f, ...args) => assocIn(o, path, f(getIn(o, path), ...args));
  295. const _ID = 'CHEAT', ID = '#'+_ID, _scroll = (s, bg='#2B2F35', thumb='grey', wk='::-webkit-scrollbar') =>
  296. `${s} {scrollbar-width:thin; scrollbar-color:${thumb} ${bg}} ${s}${wk} {width:6px; height:6px; background:${bg}} ${s}${wk}-thumb {background:${thumb}}`;
  297. GM_addStyle(`${ID} {position:fixed; bottom:0; left:0; z-index:1000; color:var(--light); background:var(--gray-dark); opacity:.75} ${ID}:hover {opacity:1}
  298. ${ID} .-frame {max-height:100vh; display:flex; flex-direction:column} ${ID} .-scrollbox {overflow-y:auto} ${_scroll(ID+" .-scrollbox")}
  299. ${ID} h3 {text-align:center} ${ID} table.-points td, ${ID} .-cheats {padding:.5ex} ${ID} .-row {display:flex; flex-direction:row}
  300. ${ID} button {background-color:var(--secondary); border-style:outset; border-radius:1em}
  301. ${ID} td.-minus button, ${ID} tr.-minus :is(.-point-name, .-point-value) {background-color:var(--danger)}
  302. ${ID} td.-plus button, ${ID} tr.-plus :is(.-point-name, .-point-value) {background-color:var(--purple)}
  303. ${ID} button.-cheats {background: var(--cyan)}`);
  304. document.body.append($cheat.UI = _node('div', {id: _ID}));
  305. $cheat.toggle = () => rf.disp(['toggle-ui']);
  306.  
  307. let _points = pointTypes => pointTypes.map(points => [points.id, points.name, points.beforeText, points.startingSum]);
  308. $store().watch(state => _points(state.app.pointTypes), points => rf.disp(['cache-points', points]));
  309. let _upd = rf.after(({show, cache, ...data}) => {$cheat.data = data});
  310.  
  311. rf.regEventDb('init-db', [_upd], (db, [_, {points={}}={}]) => ({
  312. show: false,
  313. points,
  314. cache: db.cache || {points: []},
  315. }));
  316. rf.regEventDb('toggle-ui', [_upd], db => update(db, 'show', x => !x));
  317. rf.regEventFx('point-add!', [_upd], ({db}, [_, id, n]) => ({db: updateIn(db, ['points', id], x => (x||0)+n),
  318. points: [{id, add: n}]}));
  319. rf.regEventFx('reset-cheats!', [_upd], ({db}) => ({db: merge(db, {points: {}}),
  320. points: entries(db.points).map(([id, n]) => ({id, add: -n}))}));
  321. rf.regEventDb('cache-points', [_upd], (db, [_, points]) => assocIn(db, ['cache', 'points'], points));
  322.  
  323. rf.regFx('points', changes => changes.forEach(({id, add}) => {$pointTypes().find(x => x.id == id).startingSum += add}));
  324.  
  325. rf.regSub('show', getIn);
  326. rf.regSub('points', getIn);
  327. rf.regSub('cache', getIn);
  328. rf.regSub('cheating?', db => true);
  329. rf.regSub('points*', ([_, id]) => rf.subscribe(['points', id]), n => n||0);
  330. let _change = n => (!n ? "" : `${n < 0 ? n : '+'+n}`);
  331. rf.regSub('point-show', ([_, id]) => rf.subscribe(['points', id]), _change);
  332. rf.regSub('point-changes', '<-', ['cache', 'points'], '<-', ['points'], ([points, o]) =>
  333. points.filter(([id]) => o[id]).map(([id, name, show]) => [`[${id}] ` + (show||`(${name})`), o[id]]));
  334. rf.regSub('tooltip', '<-', ['point-changes'], changes =>
  335. changes.map(([points, change]) => `${points} ${_change(change)}`).join("\n"));
  336. rf.regSub('cheating?', '<-', ['point-changes'], changes => changes.length > 0);
  337.  
  338. let PointAdd = id => n => ['button', {onclick: () => rf.disp(['point-add!', id, n])}, (n > 0) && '+', n];
  339. let Points = () => ['table.-points', ...rf.dsub(['cache', 'points']).map(([id, name, show, amount]) =>
  340. ['tr', {class: [{1: '-plus', '-1': '-minus'}[Math.sign(rf.dsub(['points*', id]))]],
  341. title: rf.dsub(['point-show', id])},
  342. ['td.-minus', ...[-100, -10, -1].map( PointAdd(id) )],
  343. ['td.-point-name', "[", ['tt', id], "]", ['br'], show||['em', "<untitled>"], ['br'], `(${name})`],
  344. ['td.-point-value', amount],
  345. ['td.-plus', ...[+100, +10, +1].map( PointAdd(id) )]])];
  346. let Frame = (...body) => ['.-frame',
  347. ['h3', {title: rf.dsub(['tooltip'])}, "Points"],
  348. ['.-scrollbox', ...body],
  349. ['div.-row', {title: rf.dsub(['tooltip'])},
  350. ['button', {onclick: $cheat}, (rf.dsub(['cheating?']) ? "< HIDE" : "× CLOSE")],
  351. rf.dsub(['cheating?']) && ['button', {onclick: () => rf.disp(['reset-cheats!'])}, "RESET"]]];
  352. let UI = () => (rf.dsub(['show']) ? [Frame, [Points]] :
  353. rf.dsub(['cheating?']) && ['button.-cheats', {onclick: $cheat, title: rf.dsub(['tooltip'])}, " Cheats: on "]);
  354.  
  355. rf.dispatchSync(['init-db']);
  356. rf.disp(['cache-points', _points( $pointTypes() )]);
  357. r.render([UI], $cheat.UI);
  358. }
  359. $cheat.toggle();
  360. }
  361. GM_registerMenuCommand("Cheat engine", $cheat);
  362. };
  363. })();